Making More Waves: Muestra

En este codelab, crearemos una muestra de audio. La app graba audio desde el micrófono integrado en el teléfono y lo reproduce.

La app graba hasta 10 segundos de audio mientras el botón Grabar está presionado. Cuando presionas Reproducir, el audio grabado se reproduce una vez (mientras mantienes presionado el botón). También puedes activar Repeticiones para que la grabación se repita una y otra vez hasta que sueltes el botón Reproducir. Cada vez que presiones Grabar, se reemplazará la grabación de audio anterior.

7eb653b71774dfed.png

Qué aprenderás

  • Conceptos básicos para crear una transmisión de grabación con baja latencia
  • Cómo almacenar y reproducir datos de audio grabados desde un micrófono

Requisitos previos

Antes de comenzar este codelab, debes considerar completar el codelab WaveMaker parte 1. Allí, se abordan algunos conceptos básicos para crear transmisiones de audio que no se analizan aquí.

Qué necesitarás

Nuestra app de ejemplo tiene cuatro componentes:

  • IU: La clase MainActivity, escrita en Java, es responsable de recibir los eventos táctiles y reenviarlos al puente JNI.
  • Puente JNI: Este archivo C++ usa JNI para proporcionar un mecanismo de comunicación entre la IU y los objetos C++. Reenvía eventos desde la IU al motor de audio.
  • Motor de audio: Esta clase C++ crea las grabaciones y reproduce transmisiones de audio.
  • Grabación de sonido: Esta clase C++ almacena los datos de audio en la memoria.

Esta es la arquitectura:

a37150c7e35aa3f8.png

Clona el proyecto

Clona el repositorio del codelab en GitHub.

git clone https://github.com/googlecodelabs/android-wavemaker2

Importa el proyecto a Android Studio

Abre Android Studio e importa el proyecto:

  • File -> New -> Import project…
  • Elige la carpeta "android-wavemaker2"

Ejecuta el proyecto

Elige la configuración de ejecución base.

f65428e71e9bdbcf.png

Luego, presiona CTRL+R para compilar y ejecutar la app de la plantilla. Debería compilarse y ejecutarse, pero no funciona. Le agregarás funcionalidad en este codelab.

Abre el módulo base

Los archivos en los que trabajarás en este codelab se almacenan en el módulo base. Expande este módulo en la ventana Project y asegúrate de que esté seleccionada la vista de Android.

cae7ee7b54407790.png

Nota: El código fuente terminado de la app de WaveMaker2 se puede encontrar en el módulo final.

El objeto SoundRecording representa datos de audio grabados en la memoria. Permite que la app escriba datos del micrófono en la memoria y los lea para reproducirlos.

Comencemos por descubrir cómo almacenar estos datos de audio.

Elige un formato de audio

Primero, tenemos que elegir un formato de audio para las muestras. AAudio admite dos formatos:

  • float: Punto flotante de precisión simple (4 bytes por muestra)
  • int16_t: Números enteros de 16 bits (2 bytes por muestra)

Para obtener una buena calidad de sonido con un volumen bajo y otros motivos, usamos muestras de float. Si la capacidad de memoria es un problema, puedes sacrificar la fidelidad y obtener espacio mediante números enteros de 16 bits.

Elige cuánto almacenamiento se necesita

Supongamos que queremos almacenar 10 segundos de datos de audio. A una tasa de muestreo de 48,000 muestras por segundo, que es la más común en dispositivos Android modernos, debemos asignar memoria para 480,000 muestras.

Abre base/cpp/SoundRecording.h y define esta constante en la parte superior del archivo.

constexpr int kMaxSamples = 480000; // 10s of audio data @ 48kHz

Define el arreglo de almacenamiento

Ahora, tenemos toda la información que necesitamos para definir un arreglo de float. Agrega la siguiente declaración a SoundRecording.h:

private:
    std::array<float,kMaxSamples> mData { 0 };

{ 0 } usa la inicialización agregada para establecer todos los valores del arreglo en 0.

Implementa write.

El método write tiene esta firma:

int32_t write(const float *sourceData, int32_t numFrames);

Este método recibe un arreglo de muestras de audio en sourceData. El tamaño del arreglo se especifica mediante numFrames. El método debe mostrar la cantidad de muestras que realmente escribe.

Se puede implementar mediante el almacenamiento del siguiente índice de escritura disponible. Inicialmente, es 0:

9b3262779d7a0a8c.png

Después de escribir cada muestra, el siguiente índice de escritura se mueve de a uno:

2971acee93b9869d.png

Esto se puede implementar fácilmente como un bucle for. Agrega el siguiente código al método write en SoundRecording.cpp

for (int i = 0; i < numSamples; ++i) {
    mData[mWriteIndex++] = sourceData[i];
}
return numSamples;

Pero ¿qué ocurre si intentamos escribir más muestras de las que podemos guardar de acuerdo al espacio? A veces, nos equivocamos. Se mostrará un error de segmentación cuando se intente acceder a un índice de arreglo fuera de los límites.

Agreguemos una verificación que cambie numSamples si mData no tiene suficiente espacio. Agrega lo siguiente arriba del código existente.

if (mWriteIndex + numSamples > kMaxSamples) {
    numSamples = kMaxSamples - mWriteIndex;
}

Implementa read.

El método read es similar a write. Almacenamos el siguiente índice de lectura.

488ab2652d0d281d.png

Auméntalo después de la lectura de una muestra.

1a7fd22f4bbb4940.png

Repetiremos esta acción hasta que hayamos leído la cantidad solicitada de muestras. ¿Qué sucede cuando llegamos al final de los datos disponibles? Hay dos comportamientos:

  • Si el bucle está habilitado, establece el índice de lectura en cero (el inicio del arreglo de datos).
  • Si se inhabilita el bucle, no hagas nada; no aumentes el índice de lectura.

789c2ce74c3a839d.png

Para estos comportamientos, necesitamos saber cuándo llegamos al final de los datos disponibles. De manera conveniente, mWriteIndex nos dice lo siguiente. Contiene la cantidad total de muestras que se escribieron en el arreglo de datos.

Con esto en mente, podemos implementar el método read en SoundRecording.cpp

int32_t framesRead = 0;
while (framesRead < numSamples && mReadIndex < mWriteIndex){
    targetData[framesRead++] = mData[mReadIndex++];
    if (mIsLooping && mReadIndex == mWriteIndex) mReadIndex = 0;
}
return framesRead;

AudioEngine realiza las siguientes tareas principales:

  • Crea una instancia de SoundRecording.
  • Crea una transmisión de la grabación para grabar datos desde el micrófono.
  • Escribe los datos grabados en la instancia SoundRecording, en la devolución de llamada de transmisión de la grabación
  • Crea una transmisión de reproducción para reproducir los datos grabados
  • Lee los datos registrados de la instancia SoundRecording en la devolución de llamada de transmisión de la grabación
  • Responde a eventos de IU para grabación, reproducción y bucle

Comencemos con la creación de la instancia SoundRecording.

Comienza con algo fácil. Crea una instancia de SoundRecording en AudioEngine.h:

private:
    SoundRecording mSoundRecording;

Tenemos dos flujos para crear: reproducción y grabación. ¿Cuál deberíamos crear primero?

Primero, debemos crear la transmisión de reproducción porque tiene solo una tasa de muestreo que proporciona la menor latencia. Una vez que la hayamos creado, podremos proporcionar su tasa de muestreo al creador de la transmisión de la grabación. De esta manera, se garantiza que las transmisiones de reproducción y grabación tengan la misma tasa de muestreo, lo que significa que no tenemos que realizar ningún remuestreo adicional de trabajo entre las transmisiones.

Propiedades de las transmisiones de reproducción

Usa estas propiedades para propagar el StreamBuilder, que creará la transmisión de la reproducción:

  • Dirección: sin especificar, se establece de manera predeterminada en la salida
  • Tasa de muestreo: no se especifica; el valor predeterminado es la tasa con la menor latencia
  • Formato: flotante
  • Recuento de canales: 2 (estéreo)
  • Modo de rendimiento: latencia baja
  • Modo de uso compartido: exclusivo

Crea la transmisión de reproducción

Ahora, tenemos todo lo que necesitamos para crear y abrir la transmisión de reproducción. Agrega el siguiente código a la parte superior del método start en AudioEngine.cpp.

// Create the playback stream.
StreamBuilder playbackBuilder = makeStreamBuilder();
AAudioStreamBuilder_setFormat(playbackBuilder.get(), AAUDIO_FORMAT_PCM_FLOAT);
AAudioStreamBuilder_setChannelCount(playbackBuilder.get(), kChannelCountStereo);
AAudioStreamBuilder_setPerformanceMode(playbackBuilder.get(), AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setSharingMode(playbackBuilder.get(), AAUDIO_SHARING_MODE_EXCLUSIVE);
AAudioStreamBuilder_setDataCallback(playbackBuilder.get(), ::playbackDataCallback, this);
AAudioStreamBuilder_setErrorCallback(playbackBuilder.get(), ::errorCallback, this);

aaudio_result_t result = AAudioStreamBuilder_openStream(playbackBuilder.get(), &mPlaybackStream);

if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error opening playback stream %s",
                       AAudio_convertResultToText(result));
   return;
}

// Obtain the sample rate from the playback stream so we can request the same sample rate from
// the recording stream.
int32_t sampleRate = AAudioStream_getSampleRate(mPlaybackStream);

result = AAudioStream_requestStart(mPlaybackStream);
if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error starting playback stream %s",
                       AAudio_convertResultToText(result));
   closeStream(&mPlaybackStream);
   return;
}

Ten en cuenta que los métodos de plantilla para los datos y las devoluciones de llamada de error se crearon para ti. Si necesitas resumir las características de estos trabajos, consulta el primer codelab.

Propiedades de la transmisión de la grabación

Usa estas propiedades para crear la transmisión de la grabación:

  • Dirección: entrada (la grabación es una entrada, mientras que la reproducción es un resultado)
  • Tasa de muestreo: lo mismo que la transmisión de salida
  • Formato: flotante
  • Recuento de canales: 1 (mono)
  • Modo de rendimiento: latencia baja
  • Modo de uso compartido: exclusivo

Crea la transmisión de la grabación

Ahora agrega el siguiente código debajo del código agregado anteriormente en start.

// Create the recording stream.
StreamBuilder recordingBuilder = makeStreamBuilder();
AAudioStreamBuilder_setDirection(recordingBuilder.get(), AAUDIO_DIRECTION_INPUT);
AAudioStreamBuilder_setPerformanceMode(recordingBuilder.get(), AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setSharingMode(recordingBuilder.get(), AAUDIO_SHARING_MODE_EXCLUSIVE);
AAudioStreamBuilder_setFormat(recordingBuilder.get(), AAUDIO_FORMAT_PCM_FLOAT);
AAudioStreamBuilder_setSampleRate(recordingBuilder.get(), sampleRate);
AAudioStreamBuilder_setChannelCount(recordingBuilder.get(), kChannelCountMono);
AAudioStreamBuilder_setDataCallback(recordingBuilder.get(), ::recordingDataCallback, this);
AAudioStreamBuilder_setErrorCallback(recordingBuilder.get(), ::errorCallback, this);

result = AAudioStreamBuilder_openStream(recordingBuilder.get(), &mRecordingStream);

if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error opening recording stream %s",
                       AAudio_convertResultToText(result));
   closeStream(&mRecordingStream);
   return;
}

result = AAudioStream_requestStart(mRecordingStream);
if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error starting recording stream %s",
                       AAudio_convertResultToText(result));
   return;
}

Ahora, pasamos a la parte divertida: almacenar los datos grabados del micrófono en el objeto SoundRecording.

Cuando creamos la transmisión de la grabación, especificamos la devolución de llamada de datos como ::recordingDataCallback. Este método llama a AudioEngine::recordingCallback, que tiene la siguiente firma:

aaudio_data_callback_result_t AudioEngine::recordingCallback(float *audioData,
                                                            int32_t numFrames)

Los datos de audio se suministran en audioData. Su tamaño (en muestras) es numFrames porque solo hay una muestra por cuadro, ya que estamos grabando en mono.

Todo lo que tenemos que hacer es lo siguiente:

  • Consulta mIsRecording para ver si se debe grabar.
  • De lo contrario, ignora los datos entrantes.
  • Si estamos grabando:
  • Proporciona audioData a SoundRecording con su método write
  • Verifica el valor de retorno de write. Si es cero, significa que SoundRecording está lleno y que deberíamos detener la grabación.
  • Se muestra AAUDIO_CALLBACK_RESULT_CONTINUE para que las devoluciones de llamada continúen.

Agrega el siguiente código a recordingCallback:

if (mIsRecording) {
    int32_t framesWritten = mSoundRecording.write(audioData, numFrames);
    if (framesWritten == 0) mIsRecording = false;
}
return AAUDIO_CALLBACK_RESULT_CONTINUE;

Al igual que la transmisión de grabación, la transmisión de reproducción llama a playbackDataCallback cuando necesita datos nuevos. Este método llama a AudioEngine::playbackCallback,, que tiene la siguiente firma:

aaudio_data_callback_result_t AudioEngine::playbackCallback(float *audioData, int32_t numFrames)

Dentro de este método, debemos hacer lo siguiente:

  • Completa el arreglo con ceros mediante fillArrayWithZeros.
  • Si estamos reproduciendo los datos registrados, indicados por mIsPlaying, luego, haz lo siguiente:
  • Lee numFrames de datos de mSoundRecording.
  • Convierte audioData de mono a estéreo con convertArrayMonoToStereo.
  • Si la cantidad de fotogramas leídos era menor que la cantidad de marcos solicitados, alcanzamos el final de los datos registrados. Detén la reproducción. Para ello, establece mIsPlaying en false.

Agrega el siguiente código a playbackCallback:

fillArrayWithZeros(audioData, numFrames * kChannelCountStereo);

if (mIsPlaying) {
   int32_t framesRead = mSoundRecording.read(audioData, numFrames);
   convertArrayMonoToStereo(audioData, framesRead);
   if (framesRead < numFrames) mIsPlaying = false;
}
return AAUDIO_CALLBACK_RESULT_CONTINUE;

Inicia y detén la grabación

El método setRecording se utiliza para iniciar y detener la grabación. Esta es su firma:

void setRecording(bool isRecording)

Cuando se presiona el botón de registro isRecording es true; cuando se suelta el botón isRecording es false.

Se usa la variable de miembro mIsRecording para activar o desactivar el almacenamiento de datos en recordingCallback. Todo lo que necesitamos hacer es establecer el valor en isRecording.

Debemos agregar un comportamiento más. Cuando se inicia la grabación, se deben reemplazar los datos existentes en mSoundRecording. Para ello, se usa clear, que restablecerá el índice de escritura de mSoundRecording a cero.

Este es el código para setRecording:

if (isRecording) mSoundRecording.clear();
mIsRecording = isRecording;

Inicia y detén la reproducción

El control de reproducción es similar a una grabación. Se llama a setPlaying cuando se presiona o se suelta el botón para reproducir. La variable de miembro mIsPlaying activa la reproducción dentro de playbackCallback.

Cuando se presiona el botón para reproducir, se inicia la reproducción al comienzo de los datos de audio grabados. Para ello, se usa setReadPositionToStart, que establece el encabezado de lectura del mSoundRecording de restablecimiento.

Este es el código para setPlaying:

if (isPlaying) mSoundRecording.setReadPositionToStart();
mIsPlaying = isPlaying;

Activa o desactiva la reproducción en bucle

Por último, cuando se activa el interruptor de repetición, se llama a setLooping. Es un paso sencillo, solo se debe pasar el argumento isOn a mSoundRecording.setLooping:

Este es el código para setLooping:

mSoundRecording.setLooping(isOn);

Las apps que graban audio deben solicitar el permiso RECORD_AUDIO al usuario. Gran parte del código de manejo de permisos ya está escrito; sin embargo, aún debemos declarar que nuestra app usa este permiso.

Agrega la siguiente línea a manifests/AndroidManifest.xml en la sección <manifest>:

<uses-permission android:name="android.permission.RECORD_AUDIO" />

Es hora de verificar si todo tu trabajo valió la pena. Compila y ejecuta la app, deberías ver la siguiente IU.

7eb653b71774dfed.png

Presiona el botón rojo para iniciar la grabación. La grabación continúa mientras mantienes presionado el botón, hasta 10 segundos como máximo. Presiona el botón verde para reproducir los datos grabados. La reproducción continúa mientras se mantiene presionado el botón hasta el final de los datos de audio. Si está habilitada la opción de repetición, los datos de audio se repiten indefinidamente.

Lecturas adicionales

Muestras de audio de alto rendimiento

Guía de audio de alto rendimiento en la documentación del NDK de Android

Prácticas recomendadas para videos de audio de Android: Google I/O 2017