Android-dev AudioRecord без блокировки или потоков
Я хочу записать аудиопоток с микрофона, чтобы на нем можно было работать в режиме реального времени.
Я хочу сделать это без использования потоков и без .read()
заблокировать, пока он ждет новых аудиоданных.
ОБНОВЛЕНИЕ / ОТВЕТ: Это ошибка в Android. 4.2.2 все еще имеет проблему, но 5.01 ИСПРАВЛЕНО! Я не уверен, где разница, но это история.
ПРИМЕЧАНИЕ. Пожалуйста, не говорите "Просто используйте темы". Потоки в порядке, но это не касается их, и разработчики Android намеревались полностью использовать AudioRecord без необходимости указывать потоки и без необходимости иметь дело с блокировкой read(). Спасибо!
Вот что я нашел:
Когда объект AudioRecord инициализируется, он создает свой собственный внутренний буфер типа кольца. когда .start()
вызывается, он начинает запись в указанный кольцевой буфер (или какой бы то ни было на самом деле.)
когда .read()
вызывается, он читает либо половину bufferSize, либо указанное число байтов (в зависимости от того, что меньше), а затем возвращает.
Если во внутреннем буфере более чем достаточно сэмплов, тогда read () мгновенно возвращает данные. Если этого пока недостаточно, read () ждет, пока он есть, и возвращает данные.
.setRecordPositionUpdateListener()
может быть использован для установки прослушивателя, и .setPositionNotificationPeriod()
а также .setNotificationMarkerPosition()
может использоваться для установки Периода уведомления и Позиции соответственно.
Однако, слушатель, кажется, никогда не вызывается, если не выполнены определенные требования:
1: Period или Position должны быть равны bufferSize/2 или (bufferSize/2) -1.
2: А .read()
должен быть вызван до начала отсчета таймера периода или позиции - другими словами, после вызова .start()
затем также позвоните .read()
и каждый раз, когда слушатель вызывается, звоните .read()
снова.
3: .read()
должен прочитать хотя бы половину bufferSize каждый раз.
Таким образом, используя эти правила, я могу заставить работать обратный вызов / прослушиватель, но по какой-то причине чтения все еще блокируются, и я не могу понять, как заставить прослушиватель вызываться только тогда, когда стоит полное чтение.
Если я настрою вид кнопки, чтобы щелкнуть, чтобы прочитать, то я могу нажать на нее, а если быстро нажать, прочитать блоки. Но если я жду, пока заполнится звуковой буфер, то первое нажатие происходит мгновенно (чтение сразу возвращается), но последующие быстрые нажатия блокируются, потому что read () должен ждать, я думаю.
Буду очень признателен за понимание того, как я мог бы заставить Слушатель работать так, как задумано - таким образом, что мой слушатель вызывается, когда достаточно данных для read () для немедленного возврата.
Ниже приведены соответствующие части моего кода.
В моем коде есть несколько операторов log, которые отправляют строки в 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");
}
};
ОБНОВЛЕНИЕ: я попробовал это на Android 2.2.3, 2.3.4, и теперь 4.0.3, и все действуют одинаково. Также: в code.google есть открытая ошибка - одна запись, начатая в 2012 году кем-то другим, а другая, созданная мной в 2013 году (я не знал о первой):
ОБНОВЛЕНИЕ 2016: Ааааа, наконец-то, после долгих лет размышлений, был ли это я или андроид, у меня наконец-то есть ответ! Я попробовал мой код выше на 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 раньше.
2 ответа
Уже поздно отвечать, но я думаю, что знаю, где Джесси допустил ошибку. Его вызов чтения блокируется, потому что он запрашивает шорты, размер которых совпадает с размером буфера, но размер буфера в байтах, а короткий содержит 2 байта. Если мы сделаем короткий массив такой же длины, что и буфер, мы будем читать вдвое больше данных.
Решение состоит в том, чтобы сделать audioData = new short[bufferSize/2]
Если размер буфера составляет 1000 байт, таким образом мы будем запрашивать 500 шорт, которые составляют 1000 байт.
Также он должен изменить samplesread=recorder.read(audioData,0,bufferSize)
в samplesread=recorder.read(audioData,0,audioData.length)
ОБНОВИТЬ
Хорошо, Джесси. Я вижу, где может быть другая ошибка - positionNotificationPeriod. Это значение должно быть достаточно большим, чтобы оно не вызывало слушателя слишком часто, и мы должны убедиться, что при вызове слушателя байты для чтения готовы для сбора. Если при вызове слушателя байты не будут готовы, основной поток будет заблокирован вызовом recordder.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 миллисекунд - это разумный интервал времени.
Теперь мы рассчитываем размер буфера на основе наших periodInFrames, поэтому любые данные не будут переопределены до того, как мы их соберем. Размер буфера выражается в байтах. Наш временной интервал составляет 4410 кадров, каждый кадр содержит 1 байт для моно или 2 байта для стерео, поэтому мы умножаем его на количество каналов (1 в вашем случае). Каждый канал содержит 1 байт для ENCODING_8BIT или 2 байта для ENCODING_16BIT, поэтому мы умножаем его на биты для выборки (16 для ENCODING_16BIT, 8 для ENCODING_8BIT) и делим его на 8.
Затем мы устанавливаем длину audioData равной половине bufferSize, поэтому мы гарантируем, что при вызове слушателя уже читаются байты, ожидающие сбора. Это потому, что short содержит 2 байта, а bufferSize выражается в байтах.
Затем мы проверяем, достаточно ли достаточно размера bufferSize для успешной инициализации объекта AudioRecord, а если нет, то устанавливаем минимальный размер bufferSize - нам не нужно изменять наш временной интервал или длину audioData.
В нашем слушателе мы читаем и храним данные в короткий массив. Вот почему мы используем audioData.length вместо размера буфера, потому что только audioData.length может сообщить нам количество шортов, содержащихся в буфере.
Я работал некоторое время назад, поэтому, пожалуйста, дайте мне знать, будет ли это работать для вас.
Я не уверен, почему вы избегаете порождения отдельных потоков, но если это происходит из-за того, что вам не нужно иметь дело с их кодированием должным образом, вы можете использовать.schedule для объекта Timer после каждого.read, где временной интервал установить время, необходимое для заполнения буфера (количество выборок в buffer / sampleRate). Да, я знаю, что это использует отдельный поток, но этот совет был дан, предполагая, что причина, по которой вы избегали использовать потоки, заключалась в том, чтобы избежать необходимости их правильного кодирования.
Таким образом, наибольшее время, в течение которого он может блокировать поток, должно быть незначительным. Но я не знаю, почему вы хотите это сделать.
Если причина не в том, почему вы избегаете использовать отдельные темы, могу я спросить почему?
Кроме того, что именно вы подразумеваете под реальным временем? Намереваетесь ли вы воспроизвести затронутый звук, скажем, с помощью AudioTrack? Потому что задержка на большинстве устройств Android довольно плохая.