2010-02-15 16 views
21

Mi aplicación Java para Android necesita grabar datos de audio en la memoria RAM y procesarlos. Es por eso que utilizo la clase "AudioRecord" y no la "MediaRecorder" (graba solo en el archivo).Android: problema de la clase AudioRecord: la devolución de llamada nunca se llama

Hasta ahora, utilicé un sondeo de bucle ocupado con "read()" para los datos de audio. esto ha estado funcionando hasta ahora, pero bloquea demasiado la CPU. Entre dos sondeos, puse el hilo en modo de suspensión para evitar el uso de la CPU al 100%. Sin embargo, esta no es realmente una solución limpia, ya que el tiempo de reposo es no garantizado y debe restar un tiempo de seguridad para no perder fragmentos de audio . Esto no es óptimo para la CPU. Necesito tantos ciclos de CPU libres como sea posible para un hilo en paralelo.

Ahora implementé la grabación usando "OnRecordPositionUpdateListener". Esto se ve muy prometedor y la forma correcta de hacerlo de acuerdo con los SDK Docs. Todo parece funcionar (abrir el dispositivo de audio, leer() los datos, etc.) pero nunca se llama al Listner.

¿Alguien sabe por qué?

Información: Estoy trabajando con un dispositivo real, no debajo del emulador. La grabación utilizando un bucle de ocupado funciona básicamente (sin embargo, no saturando). Solo el Callback Listener nunca se llama.

Aquí hay un fragmento de mi código fuente:

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); 

    } 
} 

Respuesta

13

Creo que el problema es que todavía tiene que hacer el bucle de lectura. Si configura las devoluciones de llamada, se activarán cuando haya leído la cantidad de fotogramas que especifique para las devoluciones de llamada. Pero aún necesitas hacer las lecturas. Intenté esto y las devoluciones de llamada se llaman bien. La configuración de un marcador causa una devolución de llamada cuando se ha leído ese número de fotogramas desde el inicio de la grabación. En otras palabras, puede configurar el marcador en el futuro, después de muchas de sus lecturas, y luego se disparará. Puede establecer el período en un número mayor de fotogramas y esa devolución de llamada se activará cada vez que se haya leído esa cantidad de fotogramas. Creo que lo hacen para que pueda hacer un procesamiento de bajo nivel de los datos brutos en un ciclo cerrado, y de vez en cuando su devolución de llamada puede hacer un procesamiento de nivel de resumen. Puede usar el marcador para que sea más fácil decidir cuándo dejar de grabar (en lugar de contar en el ciclo de lectura).

+0

Gracias, lo explicaste bien. – chaimp

+1

su respuesta es muy útil. Le agradecería mucho si pudiera aclararme sobre cómo leer correctamente la transmisión de entrada en vivo desde un micrófono. Un bucle while constante parece inapropiado ya que bloquearía todo el proceso, ¿verdad? Pensé que las devoluciones de llamada estaban allí como una solución, pero a juzgar por su respuesta, no lo son. Gracias. – Tom

+0

El ciclo while constante para leer datos de audio debe estar en un hilo separado. Ciertamente no puedes usar el hilo principal para leer audio. Las devoluciones de llamada se dispararían cuando se haya recopilado una cantidad adecuada de datos de audio. Podría poner la lógica de procesamiento en el bucle de lectura, o podría usar las devoluciones de llamada para activar el procesamiento en el búfer que acaba de leer.El bloque de lecturas, pero solo dentro de ese hilo, por lo que debería estar bien. –

6

Aquí está mi código usado para encontrar ruido promedio. Tenga en cuenta que se basa en las notificaciones del oyente, por lo que se ahorrará batería del dispositivo. Definitivamente se basa en los ejemplos anteriores. Esos ejemplos me ahorraron mucho tiempo, gracias.

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; 
    } 
} 
+1

Este ejemplo parece girar la CPU durante la grabación. El ciclo while (true) se ejecutará repetidamente ... – mmigdol

+0

Hola @soul, ¿por qué has establecido los periodInFrames del 'setPositionNotificationPeriod' de la misma manera que el tamaño de los buffers. –

0

Para aquellos que están grabando audio a través de un servicio de atención, también pueden experimentar este problema de devolución de llamada. He estado trabajando en este tema últimamente y se me ocurrió una solución simple en la que llama a su método de grabación en un hilo separado. Esto debería invocar el método PeriodicNotification durante la grabación sin ningún problema.

Algo como esto:

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 
      } 
     } 
    } 
5

Ahora implementa la grabación con el "OnRecordPositionUpdateListener". Esto se ve muy prometedor y el forma correcta de hacerlo de acuerdo con los SDK Docs. Todo parece funcionar (abrir el dispositivo de audio, leer() los datos, etc.) pero el Listner es nunca llamado.

¿Alguien sabe por qué?

Encontré que el OnRecordPositionUpdateListener se ignora hasta que realice su primer .read().

En otras palabras, descubrí que si configuraba todo según los documentos, mi Listener nunca recibió una llamada. Sin embargo, si llamé por primera vez a .read() justo después de hacer mi .start() inicial, se me llamará al Oyente, siempre que haga un .read() cada vez que se llame al Oyente.

En otras palabras, casi parece que el evento Listener solo sirve para una vez por .read() o algo así.

También encontré que si solicité que se leyera menos muestras de buffSize/2, no se llamaría al Listener. Por lo tanto, parece que el oyente solo se llama DESPUÉS de .read() de al menos la mitad del tamaño del búfer. Para seguir utilizando la devolución de llamada del Listener, se debe llamar a la lectura cada vez que se ejecuta el detector. (En otras palabras, ponga la llamada a leer en el código de Listener.)

Sin embargo, el oyente parece ser llamado en un momento en que los datos aún no están listos, lo que causa bloqueo.

Además, si el período de notificación o el tiempo son mayores que la mitad de su bufferSize, nunca se llamarán, al parecer.

ACTUALIZACIÓN:

A medida que continúo para cavar más profundo, he encontrado que la devolución de llamada parece ser llamado sólo cuando un .read() acabados ......!

No sé si es un error o una característica. Mi primer pensamiento sería que quiero una devolución de llamada cuando sea el momento de leer. Pero tal vez los desarrolladores de Android tuvieron la idea al revés, donde simplemente colocarías un while(1){xxxx.read(...)} en un hilo por sí mismo, y para evitar tener que llevar un registro de cada vez que read() finalizó, la devolución de llamada esencialmente puede decirte cuando una lectura ha terminado.

Oh. Tal vez me di cuenta. Y creo que otros han señalado esto antes que yo aquí, pero no se hundió: la devolución de llamada de posición o período debe operar en bytes que ya se han leído ...

Creo que tal vez estoy atascado con el uso de hilos .

En este estilo, uno tendría un hilo que llama continuamente a read() tan pronto como regresó, ya que la lectura es bloqueante y espera pacientemente a que vuelvan los datos suficientes.

Luego, independientemente, la devolución de llamada llamaría a su función especificada cada x número de muestras.

+1

Gracias por su investigación. ¡Ayuda! Un poco más sobre este problema: la devolución de llamada PeriodicNotification nunca se invoca en API 16, 17, 18, a menos que se realice una llamada de lectura() justo después del inicio. Sin embargo, parece que el problema está solucionado en API> = 19. –

+0

DmitryO, ¡Muchas gracias por la información adicional! No puedo esperar para probarlo. –

Cuestiones relacionadas