2013-04-04 3 views
4

Я хочу записать аудиопоток микрофона, чтобы я мог делать в реальном времени DSP.Android-dev AudioRecord без блокировки или потоков

Я хочу сделать это, не используя нити и не имея .read() блока, пока он ждет новых аудиоданных.

UPDATE/ANSWER: Это ошибка в Android. 4.2.2 все еще есть проблема, но 5.01 IS FIXED! Я не знаю, где этот разрыв, но это история.

ПРИМЕЧАНИЕ: Пожалуйста, не говорите «Просто используйте потоки». Нити в порядке, но это не о них, и разработчики Android, предназначенные для AudioRecord, могут быть полностью работоспособны без необходимости указывать темы и без меня иметь дело с блокировкой read(). Спасибо!

Вот что я нашел:

Когда объект AudioRecord инициализируется, он создает свой собственный внутренний буфер типа кольца. Когда .start() называется, он начинает запись в указанный кольцевой буфер (или любой другой вид, что на самом деле.)

Когда .read() называется, он читает либо половину BufferSize или указанное число байтов (в зависимости от того меньше), а затем возвращается.

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

.setRecordPositionUpdateListener() может использоваться для установки слушателя, а .setPositionNotificationPeriod() и .setNotificationMarkerPosition() могут использоваться для установки Периода и положения уведомления соответственно.

Однако Слушатель, кажется, не быть не вызывается, если определенные требования не будут выполнены:

1: Период или позиция должна быть равна BufferSize/2 или (BufferSize/2) -1.

2: .read() должен вызываться до периода или установки таймера начинает обратный отсчет - другими словами, после вызова .start() затем также называют .read(), и каждый раз, когда Слушатель называется, вызовите .read() снова.

3: .read() должен считывать по меньшей мере половину bufferSize каждый раз.

Таким образом, используя эти правила, я могу заставить обработчик обратного вызова/прослушивателя работать, но по какой-то причине чтения по-прежнему блокируются, и я не могу понять, как заставить Listener вызываться только при наличии полного чтения стоимость.

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

Очень ценно будет любое понимание того, как я мог бы заставить Слушатель работать по назначению - таким образом, чтобы мой слушатель вызывался, когда есть достаточно данных для read() для немедленного возврата.

Ниже приведены части моего кода.

У меня есть несколько операторов журнала в моем коде, которые отправляют строки для logcat, что позволяет мне видеть, сколько времени занимает каждая команда, и именно так я знаю, что read() блокирует. (А кнопки в моем простом тесте приложении также очень собачий медленно реагируют, когда он читает несколько раз, но процессор не привязан.)

Спасибо, ~ Джесси

В моей OnCreate():

bufferSize=AudioRecord.getMinBufferSize(samplerate,AudioFormat.CHANNEL_CONFIGURATION_MONO,AudioFormat.ENCODING_PCM_16BIT)*4; 
     recorder = new AudioRecord (AudioSource.MIC,samplerate,AudioFormat.CHANNEL_CONFIGURATION_MONO,AudioFormat.ENCODING_PCM_16BIT,bufferSize); 
     recorder.setRecordPositionUpdateListener(mRecordListener); 
     recorder.setPositionNotificationPeriod(bufferSize/2); 
     //recorder.setNotificationMarkerPosition(bufferSize/2); 
     audioData = new short [bufferSize]; 

    recorder.startRecording(); 
      samplesread=recorder.read(audioData,0,bufferSize);//This triggers it to start doing the callback. 

Тогда вот мой слушатель:

public OnRecordPositionUpdateListener mRecordListener = new OnRecordPositionUpdateListener() 
{ 
    public void onPeriodicNotification(AudioRecord recorder) //This one gets called every period. 
    { 
     Log.d("TimeTrack", "AAA"); 
     samplesread=recorder.read(audioData,0,bufferSize); 
     Log.d("TimeTrack", "BBB"); 
     //player.write(audioData, 0, samplesread); 
     //Log.d("TimeTrack", "CCC"); 
     reads++; 
    } 
    @Override 
    public void onMarkerReached(AudioRecord recorder) //This one gets called only once -- when the marker is reached. 
    { 
     Log.d("TimeTrack", "AAA"); 
     samplesread=recorder.read(audioData,0,bufferSize); 
     Log.d("TimeTrack", "BBB"); 
     //player.write(audioData, 0, samplesread); 
     //Log.d("TimeTrack", "CCC"); 
    } 
}; 

UPDATE: Я попробовал это на Android 2.2.3, 2.3.4, 4.0.3 и теперь, в d все действуют одинаково. Также: есть ошибка на code.google об этом - одна запись началась в 2012 году кем-то другим, затем с начала 2013 года (я не знал о первом):

UPDATE 2016: Ahhhh finally после нескольких лет удивления, был ли это я или андроид, я наконец получил ответ! Я попробовал мой код выше на 4.2.2 и такую ​​же проблему. Я пробовал код на 5.01, И ЭТО РАБОТАЕТ !!! И начальный вызов .read() НЕ нужен больше. Теперь, как только вызывается .setPositionNotificationPeriod() и .StartRecording(), mRecordListener() просто волшебным образом начинает получать вызов каждый раз, когда имеются данные , теперь, поэтому он больше не блокируется, так как обратный вызов не вызывается до после достаточно данные были записаны. Я не слушал данные, чтобы знать, правильно ли они записываются, но обратный вызов происходит так, как должен, и это не блокирует активность, как раньше!

http://code.google.com/p/android/issues/detail?id=53996

http://code.google.com/p/android/issues/detail?id=25138

Если люди, которые заботятся об этом журнале об ошибках и голосовать за и/или прокомментировать ошибка, возможно, он будет получать адрес рано от Google.

ответ

0

Я не уверен, почему вы избегаете размножения отдельных потоков, но если это не так, потому что вы не хотите иметь дело с их правильной кодировкой, вы можете использовать .schedule на объекте Timer после каждого .read, где интервал времени устанавливается на время, необходимое для заполнения вашего буфера (количество выборок в буфере/sampleRate). Да, я знаю, что это использует отдельный поток, но этот совет был дан, предполагая, что причина, по которой вы избегали использования потоков, заключалась в том, чтобы избежать необходимости правильно их кодировать.

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

Если причина не в причине, почему вы избегаете использования отдельных потоков, могу ли я спросить, почему?

Кроме того, что именно вы подразумеваете под реальным временем? Вы собираетесь воспроизводить затронутый звук, используя, скажем, AudioTrack? Потому что латентность на большинстве Android-устройств довольно плохая.

+0

Я, вероятно, в конечном итоге просто создаю нить. Кажется, что Android больше глючит, чем улей и ничего не фиксируется (25 000 новых неуправляемых ошибок дефектов, датированные годами назад некоторые из них). В ответ на ваши вопросы я хотел избежать потоков, потому что я хотел избежать беспорядочных хаков и сделать это право, если бы мог. Похоже, этого не произойдет, поэтому лучше всего использовать самый чистый хак - это еще один поток. Темы велики - не поймите меня неправильно. В реальном времени я имею в виду приложение осциллографа или анализатор спектра и т. Д. Или играю audiotrk –

0

Это опоздание, но я думаю, что знаю, где Джесси совершил ошибку. Его запрос на чтение блокируется, потому что он запрашивает шорты, размер которых равен размеру буфера, но размер буфера в байтах, а короткий - 2 байта. Если мы сделаем короткий массив равной той же длине, что и буфер, мы будем считывать вдвое больше данных.

Решение должно сделать audioData = new short[bufferSize/2] Если размер буфера составляет 1000 байт, мы запросим 500 шорт, которые составляют 1000 байт.

Кроме того, он должен изменить samplesread=recorder.read(audioData,0,bufferSize) к samplesread=recorder.read(audioData,0,audioData.length)

UPDATE

Хорошо, Джесси. Я вижу, где может быть другая ошибка - positionNotificationPeriod. Это значение должно быть достаточно большим, чтобы он не вызывал слушателя слишком часто, и мы должны убедиться, что когда слушатель называется считанными байтами, они готовы к сбору. Если байты не будут готовы при вызове слушателя, основной поток будет заблокирован вызовом recorder.read (audioData, 0, audioData.length) до тех пор, пока запрашиваемые байты не будут собраны AudioRecord. Вы должны рассчитать размер буфера и длину массива шорт на основе установленного вами временного интервала - как часто вы хотите, чтобы вызывающий слушатель вызывался. Период уведомления о позиции, размер буфера и длина массива шорт все должны быть правильно настроены. Позвольте мне показать вам пример:

int periodInFrames = sampleRate/10; 
int bufferSize = periodInFrames * 1 * 16/8; 
audioData = new short [bufferSize/2]; 

int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); 
if (bufferSize < minBufferSize) bufferSize = minBufferSize; 

recorder = new AudioRecord(AudioSource.MIC, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, buffersize); 
recorder.setRecordPositionUpdateListener(mRecordListener); 
recorder.setPositionNotificationPeriod(periodInFrames); 
recorder.startRecording(); 

public OnRecordPositionUpdateListener mRecordListener = new OnRecordPositionUpdateListener() { 
    public void onPeriodicNotification(AudioRecord recorder) { 
     samplesread = recorder.read(audioData, 0, audioData.length); 
     player.write(short2byte(audioData)); 
    } 
}; 

private byte[] short2byte(short[] data) { 
     int dataSize = data.length; 
     byte[] bytes = new byte[dataSize * 2]; 

     for (int i = 0; i < dataSize; i++) { 
      bytes[i * 2] = (byte) (data[i] & 0x00FF); 
      bytes[(i * 2) + 1] = (byte) (data[i] >> 8); 
      data[i] = 0; 
     } 
     return bytes; 
    } 

Так что теперь немного пояснений.

Сначала мы устанавливаем, как часто слушатель должен вызываться для сбора аудиоданных (periodInFrames). PositionNotificationPeriod выражается в кадрах. Частота дискретизации выражается в кадрах в секунду, поэтому для частоты дискретизации 44100 мы имеем 44100 кадров в секунду. Я разделил его на 10, чтобы слушатель вызывался каждые 4410 кадров = 100 миллисекунд - это разумный временной интервал.

Теперь мы вычисляем размер буфера на основе наших периодовInFrames, поэтому любые данные не будут переопределены, прежде чем мы его соберем. Размер буфера выражается в байтах. Наш интервал времени составляет 4410 кадров, каждый кадр содержит 1 байт для моно или 2 байта для стерео, поэтому мы умножаем его на количество каналов (1 в вашем случае). Каждый канал содержит 1 байт для ENCODING_8BIT или 2 байта для ENCODING_16BIT поэтому мы умножить его на бит на выборку (16 для ENCODING_16BIT, 8 для ENCODING_8BIT) и разделить его на 8.

Затем мы устанавливаем длину аудиоданных, чтобы быть половина BufferSize поэтому мы следим за тем, чтобы при прослушивании слушателя байты для чтения уже там ожидали сбора. Это потому, что short содержит 2 байта, а bufferSize выражается в байтах.

Затем мы проверяем, достаточно ли bufferSize достаточно для успешной инициализации объекта AudioRecord, если это не так, мы устанавливаем bufferSize на минимальный размер - нам не нужно менять наш временной интервал или длину аудиоданных.

В нашем слушателе мы считываем и храним данные в короткий массив. Вот почему мы используем audioData.length вместо размера буфера, потому что только audioData.length может сообщить нам количество шорт, которое содержит буфер.

У меня было это работает некоторое время назад, поэтому, пожалуйста, дайте мне знать, если это сработает для вас.

+0

Спасибо, Шимон! Вы пробовали исправить себя на pre-kitkat? Я как бы отказался от этого и не пробовал в последнее время. Сообщение по следующей ссылке предполагает, что проблема была установлена ​​между 4.2.1 и 4.4.2, но я еще не тестировал ее сам. Я постараюсь настроить dev env в ближайшее время и попробовать ваши исправления. https://code.google.com/p/android/issues/detail? id = 53996 Еще раз спасибо. ~ Jesse –

+0

OK Я пробовал исправить, но это не сработало. Также попробовал мое приложение на Lollipop, и у него такая же проблема. Но теперь, когда я думаю об этом, audioData - это всего лишь набор шорт. Его размер не имеет значения, пока он достаточно большой. Я попытался сделать его bufferSize/2, но это не сработало бы вообще, потому что recorder.Read проверяет размер его целевого буфера и возвращает -2, если он недостаточно большой. Если вам действительно удастся получить неблокирующие чтения, я был бы рад узнать, как это сделать! Спасибо, ~ Джесси –