Небольшая пауза между переходами циклов с движком R3 при использовании с ExoPlayer (Android)

Вопрос или проблема

Я интегрировал библиотеку Rubberband с ExoPlayer для настройки высоты и темпа в проекте Android. При использовании движка R2 аудиопетля работает гладко, как и ожидалось. Однако при переключении на движок R3 (OptionEngineFiner) с включенной петлей в exoplayer наблюдается небольшая, но заметная пауза между концом и началом аудиофайла во время переходов по петле. Это поведение не наблюдается с движком R2 (OptionEngineFaster) и затрагивает только движок R3, что приводит к несовместимому опыту петли. Есть ли что-то, что я упускаю с движком R3? Я пробовал демо-приложение Rubberband, и там петля работает нормально.

Мой код AudioProcessor для exoplayer:

rb = RubberBandStretcher(
                    inputAudioFormat.sampleRate,
                    inputAudioFormat.channelCount,
                    RubberBandStretcher.OptionProcessRealTime.or(RubberBandStretcher.OptionEngineFiner).or(RubberBandStretcher.OptionWindowShort).or(RubberBandStretcher.OptionFormantPreserved).or(RubberBandStretcher.OptionPitchHighConsistency),
                    speed.toDouble(),
                    pitch.toDouble()
                )

Весь код для Audio Processor

class RubberBandAudioProcessor : AudioProcessor {

    /** Указывает, что частота дискретизации выходного сигнала должна быть такой же, как и у входного.  */
    val SAMPLE_RATE_NO_CHANGE = -1

    /** Порог, ниже которого разница между двумя факторами высоты/скорости незначительна.  */
    private val CLOSE_THRESHOLD = 0.0001f

    /**
     * Минимальное количество выходных байтов, необходимых для расчета временной шкалы с использованием
     * входных и выходных подсчетов байтов, а не с использованием текущей скорости воспроизведения.
     */
    private val MIN_BYTES_FOR_DURATION_SCALING_CALCULATION = 1024

    private var pendingOutputSampleRate = 0
    private var speed = 1f
    private var pitch = 1f

    private var pendingInputAudioFormat: AudioProcessor.AudioFormat
    private var pendingOutputAudioFormat: AudioProcessor.AudioFormat
    private var inputAudioFormat: AudioProcessor.AudioFormat
    private var outputAudioFormat: AudioProcessor.AudioFormat

    private var pendingSonicRecreation = false
    private var rb: RubberBandStretcher? = null
    private var buffer: ByteBuffer
    private var shortBuffer: ShortBuffer
    private var outputBuffer: ByteBuffer
    private var inputBytes: Long = 0
    private var outputBytes: Long = 0
    private var inputEnded = false

    /** Создает новый аудио процессор Sonic.  */
    init {
        speed = 1f
        pitch = 1f
        pendingInputAudioFormat = AudioProcessor.AudioFormat.NOT_SET
        pendingOutputAudioFormat = AudioProcessor.AudioFormat.NOT_SET
        inputAudioFormat = AudioProcessor.AudioFormat.NOT_SET
        outputAudioFormat = AudioProcessor.AudioFormat.NOT_SET
        buffer = AudioProcessor.EMPTY_BUFFER
        shortBuffer = buffer.asShortBuffer()
        outputBuffer = AudioProcessor.EMPTY_BUFFER
        pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE
    }

    /**
     * Устанавливает целевую скорость воспроизведения. Этот метод может быть вызван только после обработки данных через
     * процессор. Значение, возвращаемое [.isActive], может измениться, и процессор должен быть
     * [очищен][.flush] перед добавлением новых данных в очередь.
     *
     * @param speed Целевой коэффициент, с которым воспроизведение должно ускориться.
     */
    fun setSpeed(speed: Float) {
        if (this.speed != speed) {
            this.speed = 1/speed
            rb?.timeRatio = 1/speed.toDouble()
            pendingSonicRecreation = true
        }
    }

    /**
     * Устанавливает целевую высоту воспроизведения. Этот метод может быть вызван только после обработки данных через
     * процессор. Значение, возвращаемое [.isActive], может измениться, и процессор должен быть
     * [очищен][.flush] перед добавлением новых данных в очередь.
     *
     * @param pitch Целевая высота.
     */
    fun setPitch(pitch: Float) {
        if (this.pitch != pitch) {
            this.pitch = pitch
            rb?.pitchScale=pitch.toDouble()
            pendingSonicRecreation = true
        }
    }

    /**
     * Устанавливает частоту дискретизации для выходного аудио в герцах. Передайте [.SAMPLE_RATE_NO_CHANGE] для вывода
     * аудио на той же частоте дискретизации, что и вход. После вызова этого метода вызовите [ ][.configure], чтобы настроить процессор с новой частотой дискретизации.
     *
     * @param sampleRateHz Частота дискретизации для выходного аудио в герцах.
     * @see .configure
     */
    fun setOutputSampleRateHz(sampleRateHz: Int) {
        pendingOutputSampleRate = sampleRateHz
    }

    /**
     * Возвращает длительность медиа, соответствующую указанной длительности воспроизведения, с учетом настройки скорости.
     *
     * Масштабирование, выполняемое этим методом, будет использовать фактическую скорость воспроизведения, достигнутую
     * аудио процессором, в среднем, с момента последнего очищения. Это может немного отличаться от
     * целевой скорости воспроизведения.
     *
     * @param playoutDuration Длительность воспроизведения для масштабирования.
     * @return Соответствующая длительность медиа, в тех же единицах, что и `duration`.
     */
    fun getMediaDuration(playoutDuration: Long): Long {
        return if (outputBytes >= MIN_BYTES_FOR_DURATION_SCALING_CALCULATION) {
            val processedInputBytes = inputBytes - Assertions.checkNotNull(rb).samplesRequired
            if (outputAudioFormat.sampleRate == inputAudioFormat.sampleRate) Util.scaleLargeTimestamp(
                playoutDuration,
                processedInputBytes,
                outputBytes
            ) else Util.scaleLargeTimestamp(
                playoutDuration,
                processedInputBytes * outputAudioFormat.sampleRate,
                outputBytes * inputAudioFormat.sampleRate
            )
        } else {
            (speed.toDouble() * playoutDuration).toLong()
        }
    }

    @Throws(UnhandledAudioFormatException::class)
    override fun configure(inputAudioFormat: AudioProcessor.AudioFormat): AudioProcessor.AudioFormat {
        if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
            throw UnhandledAudioFormatException(inputAudioFormat)
        }
        val outputSampleRateHz =
            if (pendingOutputSampleRate == SAMPLE_RATE_NO_CHANGE) inputAudioFormat.sampleRate else pendingOutputSampleRate
        pendingInputAudioFormat = inputAudioFormat
        pendingOutputAudioFormat = AudioProcessor.AudioFormat(
            outputSampleRateHz,
            inputAudioFormat.channelCount,
            C.ENCODING_PCM_16BIT
        )

        pendingSonicRecreation = true
        return pendingOutputAudioFormat
    }

    override fun isActive(): Boolean {
        val outputSampleRate = pendingOutputAudioFormat.sampleRate
        val inputSampleRate = pendingInputAudioFormat.sampleRate
        val speedDifference = abs(speed - 1f)
        val pitchDifference = abs(pitch - 1f)

        val isOutputSampleRateValid = outputSampleRate != Format.NO_VALUE
        val isSpeedChanged = speedDifference >= CLOSE_THRESHOLD
        val isPitchChanged = pitchDifference >= CLOSE_THRESHOLD
        val isSampleRateDifferent = outputSampleRate != inputSampleRate
        return (isOutputSampleRateValid
                && (isSpeedChanged || isPitchChanged || isSampleRateDifferent))
    }

    override fun queueInput(inputBuffer: ByteBuffer) {
        if (!inputBuffer.hasRemaining()) {
            return
        }
        val rb = Assertions.checkNotNull(rb)
        val shortBuffer = inputBuffer.asShortBuffer()
        val inputSize = inputBuffer.remaining()
        inputBytes += inputSize.toLong()
        rb.process(shortBuffer, inputAudioFormat.channelCount, false)
        inputBuffer.position(inputBuffer.position() + inputSize)
    }

    override fun queueEndOfStream() {
        Log.e("RUBBERBANDAUDIOPROCESSOR","QueueEndOfStream");
        if (rb != null) {
            rb!!.queueEndOfStream()
        }
        inputEnded = true
    }

    override fun getOutput(): ByteBuffer {

        val rb = rb
        val channelCount = inputAudioFormat.channelCount
        val samplesRequired = rb?.samplesRequired
        if (rb != null) {
            val outputSize = rb.available()
            if (outputSize > 0) {
                if (buffer.capacity() < outputSize * channelCount * 2) {
                    buffer = ByteBuffer.allocateDirect(outputSize * channelCount * 2)
                        .order(ByteOrder.nativeOrder())
                    shortBuffer = buffer.asShortBuffer()
                } else {
                    buffer.clear()
                    shortBuffer.clear()
                }
                val retrieved = rb.retrieve(shortBuffer, channelCount)
                outputBytes += retrieved.toLong() * channelCount * 2
                buffer.limit(retrieved * channelCount * 2)
                outputBuffer = buffer
            }
        }
        val outputBuffer = outputBuffer
        this.outputBuffer = AudioProcessor.EMPTY_BUFFER
        return outputBuffer
    }

    override fun isEnded(): Boolean {
        Log.e("inputeEnded", inputEnded.toString())
        Log.e("rb", rb.toString())
        val samplesRequired= rb?.samplesRequired

        Log.e("rb sample required", samplesRequired.toString())

       // return inputEnded && (rb == null || samplesRequired == 0)
        return inputEnded
    }

    override fun flush() {
        val isAct=isActive();
        Log.e("IsActive", isAct.toString())

        if (isAct) {
            Log.e("pendingSonicRecreation", pendingSonicRecreation.toString()  )
            inputAudioFormat = pendingInputAudioFormat
            outputAudioFormat = pendingOutputAudioFormat
            if (pendingSonicRecreation) {

                rb = RubberBandStretcher(
                    inputAudioFormat.sampleRate,
                    inputAudioFormat.channelCount,
                    RubberBandStretcher.OptionProcessRealTime.or(RubberBandStretcher.OptionEngineFiner).or(RubberBandStretcher.OptionWindowShort).or(RubberBandStretcher.OptionFormantPreserved).or(RubberBandStretcher.OptionPitchHighConsistency),
                    speed.toDouble(),
                    pitch.toDouble()
                )

            } else if (rb != null) {

                rb!!.reset()
            }
        }
        outputBuffer = AudioProcessor.EMPTY_BUFFER
        inputBytes = 0
        outputBytes = 0
        inputEnded = false
    }

    override fun reset() {
        speed = 1f
        pitch = 1f
        pendingInputAudioFormat = AudioProcessor.AudioFormat.NOT_SET
        pendingOutputAudioFormat = AudioProcessor.AudioFormat.NOT_SET
        inputAudioFormat = AudioProcessor.AudioFormat.NOT_SET
        outputAudioFormat = AudioProcessor.AudioFormat.NOT_SET
        buffer = AudioProcessor.EMPTY_BUFFER
        shortBuffer = buffer.asShortBuffer()
        outputBuffer = AudioProcessor.EMPTY_BUFFER
        pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE
        pendingSonicRecreation = false
        rb = null
        inputBytes = 0
        outputBytes = 0
        inputEnded = false
    }
}

Я пробовал изменить размер буфера.

Ответ или решение

Решение проблемы с паузами при зацикливании аудио с использованием движка R3 и ExoPlayer в Android

При интеграции библиотеки Rubberband с ExoPlayer для реализации изменения темпа и высоты тона, вы столкнулись с проблемой заметной паузы между концом и началом аудиофайла при использовании движка R3 (OptionEngineFiner) в отличие от движка R2 (OptionEngineFaster), где зацикливание происходит без задержек. Это поведение может быть связано с различиями в обработке аудио данных разными движками. Давайте рассмотрим возможные источники проблемы и способы их устранения.

Понимание Rubberband и его движков

Rubberband предлагает разные движки для обработки аудио, и каждый движок может иметь свои оптимизации и особенности. Движок R3 (OptionEngineFiner) создан для более тщательной обработки, что может взаимодействовать с параметрами зацикливания и приведением данных в корректный вид.

Возможные причины задержки

  1. Буферизация и размер буфера:

    • Возможно, ваш текущий размер буфера недостаточен для плавного воспроизведения без пауз. Попробуйте изменить размер выходного буфера. Рассмотрите возможность увеличения размера буфера в методе getOutput().
  2. Обработка данных:

    • Обратите внимание на метод обработки данных. Вы используете rb.process(), который может потребоваться оптимизировать под движок R3. Проверьте, что процесс происходит в реальном времени и не требует дополнительного времени на обработку.
  3. Проблемы синхронизации:

    • Убедитесь, что queueEndOfStream() правильно управляет законченной аудиопотоковой обработкой. Попробуйте убедиться, что данные правильно переносятся из rb.retrieve() и не теряются по пути.
  4. Неправильные параметры конфигурации аудио:

    • Возможно, необходимо пересмотреть параметры, которые передаются в RubberBandStretcher. Попробуйте изменить параметры высоты и темпа, используя более простые значения для тестирования, чтобы исключить влияние сложности настройки.

Рекомендации по улучшению кода

  1. Изменение логики зацикливания:

    • Проверьте свою реализацию зацикливания и убедитесь, что она плавно переходит от конца записи к началу. Вы можете попробовать использовать функцию getOutput() для плавного сброса и восстановления фазы сигнала.
  2. Тестирование с разнообразными аудиофайлами:

    • Попробуйте тестировать со звуковыми файлами разных длин и форматов. Это может помочь выявить, существует ли зависимость между размерами файлов и наблюдаемыми задержками.
  3. Логирование и отладка:

    • Увеличьте количество логов в критических местах для наблюдения за поведением системы. Логи могут помочь диагностировать, на каком этапе происходит замедление.
  4. Обновление библиотек:

    • Убедитесь, что используете последнюю версию Rubberband и ExoPlayer, так как новые релизы могут содержать исправления ошибок, которые могут повлиять на вашу проблему.

Заключение

Проблема с паузами между зацикливаниями может быть вызвана сочетанием различных факторов, включая, но не ограничиваясь, настройками буфера, синхронизацией и обработкой данных. Попробуйте следующие методики, чтобы определить, что именно вызывает проблемы, и примените соответствующие изменения. Это позволит вам добиться более плавного опыта при использовании R3 движка в сочетании с ExoPlayer в вашем Android приложении.

Если после применения предложенных изменений проблема не исчезнет, стоит рассмотреть возможность консультации с сообществом разработчиков или изучить документацию Rubberband более подробно для нахождения специфичных настроек, которые могут повлиять на поведение движка.

Оцените материал
Добавить комментарий

Капча загружается...