Вопрос или проблема
Я интегрировал библиотеку 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) создан для более тщательной обработки, что может взаимодействовать с параметрами зацикливания и приведением данных в корректный вид.
Возможные причины задержки
-
Буферизация и размер буфера:
- Возможно, ваш текущий размер буфера недостаточен для плавного воспроизведения без пауз. Попробуйте изменить размер выходного буфера. Рассмотрите возможность увеличения размера буфера в методе
getOutput()
.
- Возможно, ваш текущий размер буфера недостаточен для плавного воспроизведения без пауз. Попробуйте изменить размер выходного буфера. Рассмотрите возможность увеличения размера буфера в методе
-
Обработка данных:
- Обратите внимание на метод обработки данных. Вы используете
rb.process()
, который может потребоваться оптимизировать под движок R3. Проверьте, что процесс происходит в реальном времени и не требует дополнительного времени на обработку.
- Обратите внимание на метод обработки данных. Вы используете
-
Проблемы синхронизации:
- Убедитесь, что
queueEndOfStream()
правильно управляет законченной аудиопотоковой обработкой. Попробуйте убедиться, что данные правильно переносятся изrb.retrieve()
и не теряются по пути.
- Убедитесь, что
-
Неправильные параметры конфигурации аудио:
- Возможно, необходимо пересмотреть параметры, которые передаются в
RubberBandStretcher
. Попробуйте изменить параметры высоты и темпа, используя более простые значения для тестирования, чтобы исключить влияние сложности настройки.
- Возможно, необходимо пересмотреть параметры, которые передаются в
Рекомендации по улучшению кода
-
Изменение логики зацикливания:
- Проверьте свою реализацию зацикливания и убедитесь, что она плавно переходит от конца записи к началу. Вы можете попробовать использовать функцию
getOutput()
для плавного сброса и восстановления фазы сигнала.
- Проверьте свою реализацию зацикливания и убедитесь, что она плавно переходит от конца записи к началу. Вы можете попробовать использовать функцию
-
Тестирование с разнообразными аудиофайлами:
- Попробуйте тестировать со звуковыми файлами разных длин и форматов. Это может помочь выявить, существует ли зависимость между размерами файлов и наблюдаемыми задержками.
-
Логирование и отладка:
- Увеличьте количество логов в критических местах для наблюдения за поведением системы. Логи могут помочь диагностировать, на каком этапе происходит замедление.
-
Обновление библиотек:
- Убедитесь, что используете последнюю версию Rubberband и ExoPlayer, так как новые релизы могут содержать исправления ошибок, которые могут повлиять на вашу проблему.
Заключение
Проблема с паузами между зацикливаниями может быть вызвана сочетанием различных факторов, включая, но не ограничиваясь, настройками буфера, синхронизацией и обработкой данных. Попробуйте следующие методики, чтобы определить, что именно вызывает проблемы, и примените соответствующие изменения. Это позволит вам добиться более плавного опыта при использовании R3 движка в сочетании с ExoPlayer в вашем Android приложении.
Если после применения предложенных изменений проблема не исчезнет, стоит рассмотреть возможность консультации с сообществом разработчиков или изучить документацию Rubberband более подробно для нахождения специфичных настроек, которые могут повлиять на поведение движка.