Регулировать скорость воспроизведения Android AudioTrack

В настоящее время я пытаюсь воспроизвести звук с помощью AudioTrack. Аудио принимается по сети, и приложение постоянно считывает данные и добавляет их во внутренний буфер. Отдельный поток потребляет данные и использует AudioTrack для воспроизведения.

Проблемы:

  1. Воспроизведение звука колеблется (ощущение, что звук пропадает через регулярные промежутки времени), постоянно делая его нечетким.
  2. Скорость воспроизведения слишком высока или слишком низка, что делает их нереалистичными.

Чтобы избежать задержки в сети и других факторов, я заставил приложение ждать, пока оно не прочитает достаточно данных и не воспроизведет их в конце.

Это заставляет звук воспроизводиться очень быстро. Вот базовый пример логики, которую я использую.

    sampleRate = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);
    audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
                    AudioFormat.CHANNEL_OUT_STEREO,
                    AudioFormat.ENCODING_PCM_16BIT,
                    AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT),
                    AudioTrack.MODE_STREAM);
audioTrack.play();

short shortBuffer[] = new short[AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT)];
while (!stopRequested){
    readData(shortBuffer);
    audioTrack.write(shortBuffer, 0, shortBuffer.length, AudioTrack.WRITE_BLOCKING);
}

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


person Jason Nanay    schedule 13.07.2018    source источник
comment
Частота дискретизации извлекается из метода AudioTrack.getNativeOutputSampleRate() (вопрос обновлен). Я пытался установить частоту дискретизации на разные значения, но ничего из этого не сработало.   -  person Jason Nanay    schedule 13.07.2018
comment
И вы вообще не обрабатываете сигнал DSP перед его записью?   -  person WoodyDev    schedule 13.07.2018


Ответы (2)


Первая проблема, которую я вижу, это произвольная частота дискретизации.

AudioTrack.getNativeOutputSampleRate вернет частоту дискретизации, используемую звуковой системой. Это может быть 44100, 48000, 96000, 192000 или сколько угодно. Но похоже, что у вас есть аудиоданные из какого-то независимого источника, который выдает данные с очень точной частотой дискретизации.

Допустим, аудиоданные из источника дискретизируются с частотой 44 100 выборок в секунду. Если вы начнете играть в нее на 96000, она будет ускорена и выше.

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

Второй: вы уверены, что процедура readData всегда будет достаточно быстрой, чтобы успешно заполнить буфер, каким бы малым он ни был, и вернуться обратно быстрее, чем будет воспроизведен буфер?

Вы создали AudioTrack с AudioTrack.getMinBufferSize передается как параметр bufferSizeInBytes.

Функция getMinBufferSize возвращает минимально возможный размер буфера, который можно использовать при данном параметре. Допустим, он вернул размер, соответствующий буферу длиной 10 мс. Это означает, что новые данные должны быть подготовлены в течение этого интервала времени. т.е. Интервал времени между предыдущим write возвращенным управлением и выполнением нового write должен быть меньше временного размера буфера.

Таким образом, если функция readData может по какой-то причине задержаться дольше, чем этот временной интервал, воспроизведение будет приостановлено на это время, вы услышите небольшие паузы в воспроизведении.

Причины, по которым readData может задерживаться, могут быть разными: если он читает данные из файла, то он может задерживать ожидание операций ввода-вывода; если он выделяет java-объекты, он может столкнуться с задержкой сборщика мусора; если он использует какой-то декодер другого типа источника звука, который использует свою собственную буферизацию, он может периодически задерживать заполнение буфера.

Но в любом случае, если вы не создаете какой-то синтезатор реального времени, который должен реагировать как можно быстрее на пользовательский ввод, всегда используйте достаточно большой размер буфера, но не меньше возвращаемого getMinBufferSize. То есть:

sampleRate = 44100;// sampling rate of the source

int bufSize = sampleRate * 4; // 1 second length; 4 - is the frame size: 2 chanels * 2 bytes per each sample
bufSize = max(bufSize, AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT)); // Not less than getMinBufferSize returns
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
                AudioFormat.CHANNEL_OUT_STEREO,
                AudioFormat.ENCODING_PCM_16BIT,
                bufSize,
                AudioTrack.MODE_STREAM);
person AterLux    schedule 13.07.2018
comment
Большое спасибо Атерлюкс. Да, вы правы, данные отправляются потоковым устройством по RTP с постоянной частотой дискретизации. Однако у меня нет подробностей его конфигурации. Во-вторых, readData возвращает данные из буфера данных, и данные добавляются в буфер по мере их поступления в мобильное приложение. Только в небольшом временном окне, которое читается и пишется, блокировка получается. Кроме того, как чтение, так и запись из/в буфер происходят в параллельных потоках. - person Jason Nanay; 13.07.2018
comment
Глядя на настройки кодировщика потокового устройства, я вижу, что оно использует частоту дискретизации 16000. Каков метод расчета требуемого размера буфера для данных, выбранных с частотой 16000? Если недостаточно данных для заполнения всей длины буфера, должен ли я заполнить оставшуюся длину тишиной (нулем)? - person Jason Nanay; 13.07.2018
comment
Нет, вы должны заполнить столько данных, сколько у вас есть, но в третьем параметре записи вместо shortBuffer.length вы должны указать точное количество данных, возвращаемых readData. Итак, если readData заполняет только часть буфера, то он должен каким-то образом вернуть количество фактически заполненных данных. - person AterLux; 13.07.2018
comment
readData использует длину shortBuffer как минимальную длину. Если данных недостаточно, он возвращает ноль, указывающий на отсутствие данных. Если возвращается нулевая длина, я помещаю пустой массив с минимальной длиной в AudioTrack.write. - person Jason Nanay; 14.07.2018
comment
Затем вам следует пересмотреть, как readData заполняет массив. И уж точно не передавать пустые массивы в метод write. - person AterLux; 14.07.2018
comment
Пустой массив — это массив минимального размера буфера, заполненный нулями для представления тишины. Я предполагаю, что это не хорошо? - person Jason Nanay; 14.07.2018
comment
Какой должен быть размер кадра для частоты дискретизации 16000 с 2 каналами? - person Jason Nanay; 14.07.2018
comment
Если данные, то ничего писать не нужно. Запись пустого массива добавит в звук паузу тишины. Вместо этого рассмотрите возможность увеличения звукового буфера и, если данных нет, просто отложите (например, вызовом wait()) в ожидании новых данных. Размер кадра — это количество каналов, умноженное на один размер выборки в байтах. т.е. для 2-х каналов это будет: для 16-битной выборки - 4, для 8-битной - 2, для float - 8 и т.д. Размер данных, передаваемых в write, должен быть целым числом кадров - person AterLux; 14.07.2018
comment
Я уже делаю то, что вы предложили. Однако, если я пишу данные непрерывно без задержки между воспроизведением звука очень быстро. Если я добавлю задержку, она станет лучше, хотя воспроизведение будет заикаться. - person Jason Nanay; 14.07.2018
comment
Я не использовал правильный размер кадра, как вы советовали, из-за чего звук колебался. После исправления это теперь скорость воспроизведения звука постоянна. Единственная оставшаяся проблема заключается в том, что звук не звучит естественно. По-видимому, это вызвано тем, что скорость воспроизведения немного замедляется, добавляя шум. - person Jason Nanay; 15.07.2018
comment
Еще раз: было бы намного проще, если бы вы привели пример записанного звука. Проверьте размер семпла (это действительно 16 бит? не 8?) проверьте количество каналов (это стерео? не моно?) проверьте выравнивание сэмпла. Вы сказали, что у вас есть преобразование с 8 бит на 16 бит. Убедитесь, что он выровнен по образцу. т.е. половина одной выборки не переходит в другую. - person AterLux; 15.07.2018
comment
будет ли размер выборки 16 бит (2 байта) с двумя каналами означать только 1 байт на выборку для каждой стороны? - person Jason Nanay; 16.07.2018
comment
16 бит - это размер выборки. Для каждого канала они одинаковы, т.е. 2 байта на каждый канал, всего 4 байта на кадр (т.е. все каналы вместе) - person AterLux; 16.07.2018

Как сказал пользователь @pskink,

Скорее всего, ваш sampleRate (или любой другой параметр, переданный конструктору AudioTrack) недействителен.

Поэтому я бы начал с проверки того, какое значение вы на самом деле устанавливаете для частоты дискретизации.

Для справки, вы также можете установить скорость AudioTrack, вызвав метод setPlayBackParams:

public void setPlaybackParams (PlaybackParams params)

Если вы посмотрите документацию AudioTrack, вы увидите документы PlaybackParams и может установить скорость и шаг выходного звука. Затем этот объект можно передать для установки параметров воспроизведения в вашем объекте AudioTrack.

Однако маловероятно, что вам понадобится использовать это, если ваша единственная проблема — исходный конструктор sampleRate (поскольку мы не можем видеть, откуда берется переменная sampleRate).

person WoodyDev    schedule 13.07.2018
comment
Частота дискретизации извлекается из метода AudioTrack.getNativeOutputSampleRate() (добавлен к исходному вопросу). Я пытался установить частоту дискретизации на разные значения, но ничего из этого не сработало. - person Jason Nanay; 13.07.2018