Android: проблема класса AudioRecord: обратный вызов никогда не вызывается

Моему приложению Android Java необходимо записывать аудиоданные в ОЗУ и обрабатывать их. Вот почему я использую класс «AudioRecord», а не «MediaRecorder» (запись только в файл).

До сих пор я использовал опрос цикла занятости с «read ()» для аудиоданных. до сих пор это работало, но слишком сильно нагружало процессор. Между двумя опросами я усыпил поток, чтобы избежать 100% загрузки ЦП. Однако это не совсем чистое решение, поскольку время сна не гарантируется, и вы должны вычесть время безопасности, чтобы не потерять фрагменты аудио. Это не оптимально для процессора. Мне нужно как можно больше свободных циклов процессора для параллельного потока.

Теперь я реализовал запись с помощью «OnRecordPositionUpdateListener». Это выглядит очень многообещающе, и это правильный способ сделать это в соответствии с SDK Docs. Кажется, все работает (открытие аудиоустройства, чтение () данных и т. д.), но Listner никогда не вызывается.

Кто-нибудь знает, почему?

Информация: я работаю с реальным устройством, а не с эмулятором. Запись с использованием Busy Loop в основном работает (но не удовлетворяет). Никогда не вызывается только прослушиватель обратного вызова.

Вот фрагмент из моего исходного кода:

public class myApplication extends Activity {

  /* audio recording */
  private static final int AUDIO_SAMPLE_FREQ = 16000;
  private static final int AUDIO_BUFFER_BYTESIZE = AUDIO_SAMPLE_FREQ * 2 * 3; // = 3000ms
  private static final int AUDIO_BUFFER_SAMPLEREAD_SIZE = AUDIO_SAMPLE_FREQ  / 10 * 2; // = 200ms

  private short[] mAudioBuffer = null; // audio buffer
  private int mSamplesRead; // how many samples are recently read
  private AudioRecord mAudioRecorder; // Audio Recorder

  ...

  private OnRecordPositionUpdateListener mRecordListener = new OnRecordPositionUpdateListener() {

    public void onPeriodicNotification(AudioRecord recorder) {
      mSamplesRead = recorder.read(mAudioBuffer, 0, AUDIO_BUFFER_SAMPLEREAD_SIZE);
      if (mSamplesRead > 0) {

        // do something here...

      }
    }

    public void onMarkerReached(AudioRecord recorder) {
      Error("What? Hu!? Where am I?");
    }
  };

  ...

  public void onCreate(Bundle savedInstanceState) {

  try {
      mAudioRecorder = new AudioRecord(
          android.media.MediaRecorder.AudioSource.MIC, 
          AUDIO_SAMPLE_FREQ,
          AudioFormat.CHANNEL_CONFIGURATION_MONO,
          AudioFormat.ENCODING_PCM_16BIT,  
          AUDIO_BUFFER_BYTESIZE);
    } catch (Exception e) {
      Error("Unable to init audio recording!");
    }

    mAudioBuffer = new short[AUDIO_BUFFER_SAMPLEREAD_SIZE];
    mAudioRecorder.setPositionNotificationPeriod(AUDIO_BUFFER_SAMPLEREAD_SIZE);
    mAudioRecorder.setRecordPositionUpdateListener(mRecordListener);
    mAudioRecorder.startRecording();

    /* test if I can read anything at all... (and yes, this here works!) */
    mSamplesRead = mAudioRecorder.read(mAudioBuffer, 0, AUDIO_BUFFER_SAMPLEREAD_SIZE);

  }
}

person Thilo Köhler    schedule 15.02.2010    source источник


Ответы (4)


Я считаю, что проблема в том, что вам все еще нужно выполнить цикл чтения. Если вы настроите обратные вызовы, они сработают, когда вы прочитаете количество кадров, указанное для обратных вызовов. Но читать все равно нужно. Я пробовал это, и обратные вызовы вызываются просто отлично. Установка маркера вызывает обратный вызов, когда с начала записи было считано это количество кадров. Другими словами, вы можете установить маркер далеко в будущее, после многих ваших прочтений, и тогда он сработает. Вы можете установить период на большее количество кадров, и этот обратный вызов будет срабатывать каждый раз, когда будет прочитано это количество кадров. Я думаю, что они делают это, чтобы вы могли выполнять низкоуровневую обработку необработанных данных в тесном цикле, а затем время от времени ваш обратный вызов может выполнять обработку сводного уровня. Вы можете использовать маркер, чтобы было проще решить, когда остановить запись (вместо подсчета в цикле чтения).

person Dave MacLean    schedule 28.09.2010
comment
Спасибо - вы хорошо объяснили это. - person chaimp; 21.12.2010
comment
ваш ответ очень полезен. Я был бы очень признателен, если бы вы могли просветить меня о том, как правильно читать входной поток в реальном времени с микрофона. Постоянный цикл while кажется неуместным, поскольку он блокирует весь процесс, верно? Я думал, что обратные вызовы есть как решение, но, судя по вашему ответу, их нет. Спасибо. - person Tom; 25.01.2011
comment
Постоянный цикл while для чтения аудиоданных должен быть в отдельном потоке. Вы, конечно, не можете использовать основной поток для чтения аудио. Обратные вызовы будут срабатывать, когда будет собрано соответствующее количество аудиоданных. Вы можете поместить логику обработки в цикл чтения или использовать обратные вызовы для запуска обработки в буфере, который вы только что прочитали. Блок чтения, но только внутри этого потока, так что все должно быть в порядке. - person Dave MacLean; 26.01.2011
comment
Похоже, setRecordPositionUpdateListener вообще не нужен. Это добавляет гораздо больше путаницы, чем пользы. - person alehro; 21.06.2011

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

private AudioRecord recorder;
private boolean recorderStarted;
private Thread recordingThread;
private int bufferSize = 800;
private short[][] buffers = new short[256][bufferSize];
private int[] averages = new int[256];
private int lastBuffer = 0;

protected void startListenToMicrophone() {
    if (!recorderStarted) {

        recordingThread = new Thread() {
            @Override
            public void run() {
                int minBufferSize = AudioRecord.getMinBufferSize(8000, AudioFormat.CHANNEL_CONFIGURATION_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
                recorder = new AudioRecord(AudioSource.MIC, 8000, AudioFormat.CHANNEL_CONFIGURATION_MONO,
                        AudioFormat.ENCODING_PCM_16BIT, minBufferSize * 10);
                recorder.setPositionNotificationPeriod(bufferSize);
                recorder.setRecordPositionUpdateListener(new OnRecordPositionUpdateListener() {
                    @Override
                    public void onPeriodicNotification(AudioRecord recorder) {
                        short[] buffer = buffers[++lastBuffer % buffers.length];
                        recorder.read(buffer, 0, bufferSize);
                        long sum = 0;
                        for (int i = 0; i < bufferSize; ++i) {
                            sum += Math.abs(buffer[i]);
                        }
                        averages[lastBuffer % buffers.length] = (int) (sum / bufferSize);
                        lastBuffer = lastBuffer % buffers.length;
                    }

                    @Override
                    public void onMarkerReached(AudioRecord recorder) {
                    }
                });
                recorder.startRecording();
                short[] buffer = buffers[lastBuffer % buffers.length];
                recorder.read(buffer, 0, bufferSize);
                while (true) {
                    if (isInterrupted()) {
                        recorder.stop();
                        recorder.release();
                        break;
                    }
                }
            }
        };
        recordingThread.start();

        recorderStarted = true;
    }
}

private void stopListenToMicrophone() {
    if (recorderStarted) {
        if (recordingThread != null && recordingThread.isAlive() && !recordingThread.isInterrupted()) {
            recordingThread.interrupt();
        }
        recorderStarted = false;
    }
}
person soul    schedule 04.11.2011
comment
Этот пример, кажется, раскручивает процессор во время записи. Цикл while(true) будет выполняться повторно... - person mmigdol; 27.11.2013
comment
Привет, @soul, почему ты установил периодInFrames для setPositionNotificationPeriod таким же, как размер буфера. - person Fouad Wahabi; 04.12.2014

Теперь я реализовал запись с помощью «OnRecordPositionUpdateListener». Это выглядит очень многообещающе, и это правильный способ сделать это в соответствии с SDK Docs. Кажется, все работает (открытие аудиоустройства, чтение () данных и т. д.), но Listner никогда не вызывается.

Кто-нибудь знает, почему?

Я обнаружил, что OnRecordPositionUpdateListener игнорируется до тех пор, пока вы не сделаете свой первый .read().

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

Другими словами, похоже, что событие Listener подходит только один раз за .read() или что-то в этом роде.

Я также обнаружил, что если я запрошу для чтения любые сэмплы меньше, чем buffSize/2, слушатель не будет вызван. Таким образом, кажется, что слушатель вызывается только ПОСЛЕ .read() по крайней мере половины размера буфера. Чтобы продолжать использовать обратный вызов прослушивателя, необходимо вызывать read каждый раз при запуске прослушивателя. (Другими словами, поместите вызов read в код прослушивателя.)

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

Кроме того, если ваш период или время уведомления больше половины вашего bufferSize, похоже, они никогда не будут вызваны.

ОБНОВИТЬ:

По мере того, как я продолжаю копать глубже, я обнаружил, что обратный вызов, кажется, вызывается ТОЛЬКО, когда заканчивается .read() ......!

Не знаю, баг это или фича. Моя первоначальная мысль заключалась в том, что я хочу получить обратный вызов, когда придет время читать. Но, возможно, у разработчиков Android была идея наоборот, где вы просто поместите while(1){xxxx.read(...)} в поток отдельно, и чтобы избавить вас от необходимости отслеживать каждый раз, когда read() завершено, обратный вызов может по существу сказать вам когда чтение закончилось.

Ой. Может, меня просто осенило. И я думаю, что другие указывали на это до меня здесь, но это не дошло: обратный вызов позиции или точки должен работать с байтами, которые уже были прочитаны...

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

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

Затем, независимо, обратный вызов будет вызывать указанную вами функцию через каждые x отсчетов.

person Jesse Gordon    schedule 04.04.2013
comment
Спасибо за ваше исследование. Это помогает! Еще немного об этой проблеме: обратный вызов onPeriodicNotification никогда не вызывается в API 16, 17, 18, если только не сделать вызов read() сразу после запуска. Однако похоже, что проблема устранена на API ›=19. - person DmitryO.; 25.02.2016
comment
DmitryO, Большое спасибо за дополнительную информацию! Я не могу дождаться, чтобы попробовать это. - person Jesse Gordon; 26.02.2016

Для тех, кто записывает аудио через службу Intent, они также могут столкнуться с этой проблемой обратного вызова. В последнее время я работал над этой проблемой и придумал простое решение, в котором вы вызываете свой метод записи в отдельном потоке. Это должно вызывать метод onPeriodicNotification во время записи без каких-либо проблем.

Что-то вроде этого:

public class TalkService extends IntentService {
...
@Override
protected void onHandleIntent(Intent intent) {
Context context = getApplicationContext();
tRecord = new Thread(new recordAudio());
tRecord.start();
...
while (tRecord.isAlive()) {
    if (getIsDone()) {
        if (aRecorder.getRecordingState() == AudioRecord.RECORDSTATE_STOPPED) {
            socketConnection(context, host, port);
        }
    }
} }
...
class recordAudio implements Runnable {

        public void run() {
            try {
                OutputStream osFile = new FileOutputStream(file);
                BufferedOutputStream bosFile = new BufferedOutputStream(osFile);
                DataOutputStream dosFile = new DataOutputStream(bosFile);

                aRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC,
                        sampleRate, channelInMode, encodingMode, bufferSize);

                data = new short[bufferSize];

                aRecorder.setPositionNotificationPeriod(sampleRate);
                aRecorder
                        .setRecordPositionUpdateListener(new AudioRecord.OnRecordPositionUpdateListener() {
                            int count = 1;

                            @Override
                            public void onPeriodicNotification(
                                    AudioRecord recorder) {
                                Log.e(WiFiDirect.TAG, "Period notf: " + count++);

                                if (getRecording() == false) {
                                    aRecorder.stop();
                                    aRecorder.release();
                                    setIsDone(true);
                                    Log.d(WiFiDirect.TAG,
                                            "Recorder stopped and released prematurely");
                                }
                            }

                            @Override
                            public void onMarkerReached(AudioRecord recorder) {
                                // TODO Auto-generated method stub

                            }
                        });

                aRecorder.startRecording();
                Log.d(WiFiDirect.TAG, "start Recording");
                aRecorder.read(data, 0, bufferSize);
                for (int i = 0; i < data.length; i++) {
                    dosFile.writeShort(data[i]);
                }

                if (aRecorder.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                    aRecorder.stop();
                    aRecorder.release();
                    setIsDone(true);
                    Log.d(WiFiDirect.TAG, "Recorder stopped and released");
                }

            } catch (Exception e) {
                // TODO: handle exception
            }
        }
    }
person misterbaykal    schedule 28.05.2012