AAudio es una API de Android C nueva que se introdujo en la actualización de Android O. Está diseñada para aplicaciones de audio de alto rendimiento que requieren baja latencia. Las apps se comunican con AAudio mediante la lectura y escritura de datos en transmisiones.
La API de AAudio tiene un diseño mínimo, por lo que no realiza estas funciones:
- Enumeración de dispositivos de audio
- Enrutamiento automatizado entre extremos de audio
- E/S de archivos
- Decodificación de audio comprimido
- Presentación automática de todas las entradas/transmisiones en una sola devolución de llamada
Cómo comenzar
Puedes llamar a AAudio desde código C++. Para agregar el conjunto de atributos de AAudio a tu app, incluye el archivo de encabezado de AAudio.h:
#include <aaudio/AAudio.h>
Transmisiones de audio
AAudio transfiere los datos de audio entre tu app y las entradas y salidas de audio en el dispositivo Android. Tu app recibe y envía datos leyendo transmisiones de audio y escribiendo en ellas, y estas se encuentran representadas por la estructura AAudioStream. Estas llamadas de lectura/escritura pueden incluir bloqueo o no.
Una transmisión se define conforme a lo siguiente:
- El dispositivo de audio, que es la fuente o el receptor de los datos de la transmisión.
- El modo de uso compartido, que determina si una transmisión tiene acceso exclusivo a un dispositivo de audio que, de lo contrario, puede compartirse entre varias transmisiones
- El formato de los datos de audio en la transmisión.
Dispositivo de audio
Cada transmisión se encuentra vinculada a un único dispositivo de audio.
Un dispositivo de audio es una interfaz de hardware o un extremo virtual que actúa como fuente o receptor de una transmisión continua de datos de audio digital. No confundas un dispositivo de audio (un micrófono integrado o auriculares Bluetooth) con el dispositivo Android (el teléfono o reloj) que ejecuta tu app.
Puedes utilizar el método getDevices()
de AudioManager
para descubrir los dispositivos de audio disponibles en tu dispositivo Android. El método muestra información acerca del objeto type
de cada dispositivo.
Cada dispositivo de audio tiene un ID único en el dispositivo Android. Puedes utilizar el ID para vincular una transmisión de audio a un dispositivo de audio específico. Sin embargo, puedes permitir que AAudio elija el dispositivo principal predeterminado en la mayoría de los casos, en lugar de especificarlo tú.
El dispositivo de audio vinculado a una transmisión determina si la transmisión es de entrada o salida. Una transmisión puede transferir datos en una sola dirección. Cuando defines una transmisión, también estableces su dirección. Cuando abres una transmisión, Android comprueba que la dirección del dispositivo de audio y de la transmisión concuerden.
Modo de uso compartido
Una transmisión tiene un modo de uso compartido:
AAUDIO_SHARING_MODE_EXCLUSIVE
significa que la transmisión tiene acceso exclusivo al dispositivo de audio; ninguna otra transmisión de audio puede utilizar el dispositivo. Si el dispositivo de audio ya está en uso, quizás no sea posible que la transmisión tenga acceso exclusivo. Es probable que las transmisiones exclusivas tengan una menor latencia, pero también es más probable que se desconecten. Debes cerrar las transmisiones exclusivas tan pronto dejes de necesitarlas, de modo que otras apps puedan acceder al dispositivo. Las transmisiones exclusivas proporcionan la menor latencia posible.AAUDIO_SHARING_MODE_SHARED
permite que AAudio combine audio. AAudio combina todas las transmisiones compartidas asignadas al mismo dispositivo.
Puedes configurar el modo de uso compartido de forma explícita cuando creas una transmisión. De forma predeterminada, el modo de uso compartido es SHARED
.
Formato de audio
Los datos que se transfieren por medio de una transmisión tienen los atributos de audio digital habituales. Son los siguientes:
- Formato de datos de muestra
- Recuento de canales (muestras por fotograma)
- Tasa de muestreo
AAudio permite los siguientes formatos de muestra:
aaudio_format_t | Tipo de datos C | Notas |
---|---|---|
AAUDIO_FORMAT_PCM_I16 | int16_t | muestras comunes de 16 bits, formato Q0.15 |
AAUDIO_FORMAT_PCM_FLOAT | float | -1.0 a +1.0 |
AAUDIO_FORMAT_PCM_I24_PACKED | uint8_t en grupos de 3 | muestras empaquetadas de 24 bits, formato Q0.23 |
AAUDIO_FORMAT_PCM_I32 | int32_t | muestras comunes de 32 bits, formato Q0.15 |
AAUDIO_FORMAT_IEC61937 | uint8_t | audio comprimido unido a IEC61937 para transferencia HDMI o S/PDIF |
Si solicitas un formato de muestra específico, la transmisión usará ese formato, incluso si no es óptimo para el dispositivo. Si no especificas un formato de muestra, AAudio elegirá uno óptimo. Después de que se abra la transmisión, debes buscar el formato de datos de muestra y convertir los datos si es necesario, como se indica en este ejemplo:
aaudio_format_t dataFormat = AAudioStream_getDataFormat(stream);
//... later
if (dataFormat == AAUDIO_FORMAT_PCM_I16) {
convertFloatToPcm16(...)
}
Cómo crear una transmisión de audio
La biblioteca de AAudio respeta un patrón de diseño compilador y proporciona AAudioStreamBuilder.
- Crea un objeto AAudioStreamBuilder:
AAudioStreamBuilder *builder; aaudio_result_t result = AAudio_createStreamBuilder(&builder);
- Establece la configuración de la transmisión de audio en el compilador mediante las funciones del compilador que corresponden a los parámetros de la transmisión. Se encuentran disponibles las siguientes funciones opcionales de configuración:
AAudioStreamBuilder_setDeviceId(builder, deviceId); AAudioStreamBuilder_setDirection(builder, direction); AAudioStreamBuilder_setSharingMode(builder, mode); AAudioStreamBuilder_setSampleRate(builder, sampleRate); AAudioStreamBuilder_setChannelCount(builder, channelCount); AAudioStreamBuilder_setFormat(builder, format); AAudioStreamBuilder_setBufferCapacityInFrames(builder, frames);
Ten en cuenta que estos métodos no informan errores, como una constante sin definir o un valor fuera de rango.
Si no especificas el deviceId, el valor predeterminado será el dispositivo de salida principal. Si no especificas la dirección de transmisión, el valor predeterminado será una transmisión de salida. En cuanto a los demás parámetros, puedes configurar explícitamente un valor o dejar que el sistema asigne el valor óptimo si no especificas ningún parámetro o lo configuras como
AAUDIO_UNSPECIFIED
.Para estar seguro, comprueba el estado de la transmisión de audio luego de crearla, como se explica en el paso 4 a continuación.
- Si AAudioStreamBuilder está configurado, utilízalo para crear una transmisión:
AAudioStream *stream; result = AAudioStreamBuilder_openStream(builder, &stream);
- Luego de crear la transmisión, verifica la configuración. Si especificaste el formato de la muestra, la tasa de muestreo o las muestras por marco, no se modificarán. Si especificaste el modo de uso compartido o la capacidad de búfer, es posible que se modifiquen según las capacidades del dispositivo de audio de la transmisión y del dispositivo Android en el cual se ejecuta. Por una cuestión de buena programación defensiva, debes comprobar la configuración de la transmisión antes de usarla. Hay funciones para recuperar la configuración de la transmisión que corresponde a cada configuración del compilador:
- Puedes guardar el compilador y volver a utilizarlo en el futuro para crear más transmisiones. Pero si no tienes planes de volver a utilizarlo, deberías borrarlo.
AAudioStreamBuilder_delete(builder);
Cómo usar una transmisión de audio
Transiciones de estado
Por lo general, una transmisión de AAudio se encuentra en uno de cinco estados estables (el estado de error, Disconnected, se describe al final de esta sección):
- Open
- Started
- Paused
- Flushed
- Stopped
Los datos solo fluyen a través de una transmisión cuando está en estado Started. Para cambiar el estado de una transmisión, usa una de las funciones que solicitan una transición de estado:
aaudio_result_t result;
result = AAudioStream_requestStart(stream);
result = AAudioStream_requestStop(stream);
result = AAudioStream_requestPause(stream);
result = AAudioStream_requestFlush(stream);
Ten en cuenta que solo puedes solicitar una pausa o un vaciamiento en una transmisión de salida:
Estas funciones son asíncronas, y el cambio de estado no se produce de inmediato. Cuando solicitas un cambio de estado, la transmisión transfiere uno de los estados transitorios correspondientes:
- Starting
- Pausing
- Flushing
- Stopping
- Closing
En el diagrama de estados a continuación, se muestran los estados estables como rectángulos redondeados y los estados transitorios como rectángulos en línea de puntos.
Si bien no se muestra, puedes llamar a close()
desde cualquier estado.
AAudio no proporciona devoluciones de llamada para alertarte sobre los cambios de estado. Se puede usar una función especial, AAudioStream_waitForStateChange(stream, inputState, nextState, timeout)
, para esperar un cambio de estado.
La función no detecta un cambio de estado por su cuenta ni espera un estado específico. Espera hasta que el estado actual sea diferente de inputState
, que debes especificar.
Por ejemplo, después de solicitar una pausa, una transmisión debe ingresar inmediatamente en el estado transitorio Pausing y luego llegar al estado Paused, aunque no hay garantías de que lo hará.
Como no puedes esperar el estado Paused, usa waitForStateChange()
para esperar cualquier estado distinto de Pausing. Aquí te mostramos cómo hacerlo:
aaudio_stream_state_t inputState = AAUDIO_STREAM_STATE_PAUSING;
aaudio_stream_state_t nextState = AAUDIO_STREAM_STATE_UNINITIALIZED;
int64_t timeoutNanos = 100 * AAUDIO_NANOS_PER_MILLISECOND;
result = AAudioStream_requestPause(stream);
result = AAudioStream_waitForStateChange(stream, inputState, &nextState, timeoutNanos);
Si el estado de la transmisión no es Pausing (el inputState
, que asumimos que era el estado actual en el momento de la llamada), la función muestra un resultado de inmediato. De lo contrario, se bloquea hasta que el estado ya no sea Pausing o hasta que se acabe el tiempo de espera. Cuando la función muestra un resultado, el parámetro nextState
muestra el estado actual de la transmisión.
Puedes usar la misma técnica después de llamar a la operación de inicio, detención o vaciamiento de una solicitud, con el estado transitorio correspondiente como el inputState. No llames a waitForStateChange()
luego de llamar a AAudioStream_close()
, ya que se borrará la transmisión en cuanto se cierre. No llames a AAudioStream_close()
mientras waitForStateChange()
se ejecuta en otro subproceso.
Cómo leer una transmisión de audio y escribir en ella
Existen dos maneras de procesar los datos en una transmisión una vez que inició:
- Usa una devolución de llamada de prioridad alta.
- Usa las funciones
AAudioStream_read(stream, buffer, numFrames, timeoutNanos)
yAAudioStream_write(stream, buffer, numFrames, timeoutNanos)
para leer la transmisión o escribir en ella.
Para un bloqueo de lectura o escritura que transfiera la cantidad de tramas especificada, configura timeoutNanos con un valor superior a cero. Para una llamada sin bloqueo, configura timeoutNanos en cero. En este caso, el resultado es el número real de tramas transferidas.
Cuando leas entradas, debes verificar que se haya leído el número correcto de marcos. De lo contrario, el búfer podría contener datos desconocidos que podrían provocar una falla de audio. Puedes rellenar el búfer con ceros para crear un abandono silencioso:
aaudio_result_t result =
AAudioStream_read(stream, audioData, numFrames, timeout);
if (result < 0) {
// Error!
}
if (result != numFrames) {
// pad the buffer with zeros
memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
}
Puedes escribir datos o silencios en la transmisión para preparar el búfer de la transmisión antes de comenzar. Esto debe realizarse en una llamada sin bloqueo con timeoutNanos configurado en cero.
Los datos del búfer deben coincidir con el formato de datos que muestra AAudioStream_getDataFormat()
.
Cómo cerrar una transmisión de audio
Cuando hayas terminado de usar una transmisión, ciérrala:
AAudioStream_close(stream);
Después de cerrar una transmisión, no podrás usarla con ninguna función basada en transmisión de AAudio.
Transmisión de audio desconectada
Una transmisión de audio puede desconectarse en cualquier momento si ocurre alguno de los eventos siguientes:
- El dispositivo de audio asociado ya no está conectado (por ejemplo, cuando se desconectan los auriculares).
- Se produce un error interno.
- Un dispositivo de audio ya no es el dispositivo de audio principal.
Cuando una transmisión se desconecta, tiene el estado "Disconnected", y los intentos por ejecutar AAudioStream_write() y otras funciones mostrarán un error. Siempre debes detener y cerrar una transmisión desconectada, independientemente del código de error que obtengas.
Si usas una devolución de llamada de datos (a diferencia de uno de los métodos directos de lectura o escritura), no obtendrás ningún código de respuesta cuando la transmisión se desconecte. Para que se te notifique cuando esto suceda, escribe una función AAudioStream_errorCallback y regístrala con AAudioStreamBuilder_setErrorCallback().
Si recibes una notificación sobre la desconexión en un subproceso de devolución de llamada de error, la detención y el cierre de la transmisión deberán realizarse desde otro subproceso. De lo contrario, podría generarse un interbloqueo.
Ten en cuenta que, si abres una transmisión nueva, puede tener una configuración diferente a la de la transmisión original (por ejemplo, framesPerBurst):
void errorCallback(AAudioStream *stream,
void *userData,
aaudio_result_t error) {
// Launch a new thread to handle the disconnect.
std::thread myThread(my_error_thread_proc, stream, userData);
myThread.detach(); // Don't wait for the thread to finish.
}
Optimización del rendimiento
Si deseas optimizar el rendimiento de una aplicación de audio, puedes ajustar los búferes internos y utilizar subprocesos de prioridad alta especiales.
Cómo ajustar los búferes para minimizar la latencia
AAudio envía y recibe datos en los búferes internos que mantiene, uno para cada dispositivo de audio.
La capacidad del búfer es la cantidad total de datos que puede retener. Puedes llamar a AAudioStreamBuilder_setBufferCapacityInFrames()
para configurar la capacidad. El método limita la capacidad que puedes asignar al valor máximo que permite el dispositivo. Usa AAudioStream_getBufferCapacityInFrames()
para verificar la capacidad real del búfer.
Una app no debe utilizar la capacidad total de un búfer. AAudio llenará un búfer hasta un tamaño que puedes configurar. El tamaño de un búfer no puede superar su capacidad y, por lo general, es más pequeño. Mediante el control del tamaño del búfer, determinas la cantidad necesaria de picos de actividad para llenarlo y, por lo tanto, controlas la latencia. Usa los métodos AAudioStreamBuilder_setBufferSizeInFrames()
y AAudioStreamBuilder_getBufferSizeInFrames()
para trabajar con el tamaño del búfer.
Cuando una aplicación reproduce audio, escribe en un búfer y lo bloquea hasta completar la escritura. AAudio lee del búfer en picos de actividad discretos. Cada pico de actividad contiene varias tramas de audio y, por lo general, su tamaño es inferior al del búfer que se lee. El sistema controla la tasa y el tamaño de los picos de actividad; el circuito del dispositivo de audio es el que generalmente determina estas propiedades. Si bien no puedes cambiar el tamaño ni la frecuencia de los picos de actividad, puedes configurar el tamaño del búfer interno conforme a la cantidad de picos de actividad que contiene. Por lo general, se obtiene la menor latencia si el tamaño del búfer de AAudioStream es múltiplo del tamaño del pico de actividad informado.
Una manera de optimizar el tamaño del búfer es comenzar con un búfer grande y reducirlo gradualmente hasta llegar al agotamiento y luego volver a incrementarlo para corregirlo. Como alternativa, puedes comenzar con un tamaño de búfer pequeño y, si se agota, aumentar el tamaño del búfer hasta que los datos de salida fluyan sin problema nuevamente.
Este proceso puede ser muy rápido, incluso puede completarse antes de que el usuario reproduzca el primer sonido. Se recomienda que realices primero la determinación del tamaño del búfer inicial, con silencio, de modo que el usuario no escuche fallas de audio. Es posible que el rendimiento del sistema cambie con el tiempo (por ejemplo, el usuario puede desactivar el modo de avión). Como el ajuste del búfer suma muy poca sobrecarga, tu app puede hacerlo continuamente mientras la app lee o escribe datos en una transmisión.
A continuación, te mostramos un ejemplo de un bucle de optimización del búfer:
int32_t previousUnderrunCount = 0;
int32_t framesPerBurst = AAudioStream_getFramesPerBurst(stream);
int32_t bufferSize = AAudioStream_getBufferSizeInFrames(stream);
int32_t bufferCapacity = AAudioStream_getBufferCapacityInFrames(stream);
while (go) {
result = writeSomeData();
if (result < 0) break;
// Are we getting underruns?
if (bufferSize < bufferCapacity) {
int32_t underrunCount = AAudioStream_getXRunCount(stream);
if (underrunCount > previousUnderrunCount) {
previousUnderrunCount = underrunCount;
// Try increasing the buffer size by one burst
bufferSize += framesPerBurst;
bufferSize = AAudioStream_setBufferSize(stream, bufferSize);
}
}
}
Utilizar esta técnica para optimizar el tamaño del búfer de una transmisión de entrada no conlleva ninguna ventaja. Las transmisiones de entrada se ejecutan lo más rápido posible y tratan de minimizar la cantidad de datos almacenados en el búfer para después completarlo cuando se interrumpe la app.
Cómo usar una devolución de llamada de alta prioridad
Si tu app lee o escribe datos de audio de un subproceso común, es posible que se la evite o experimente fluctuación en el tiempo. Esto puede causar fallas de audio. Los búferes más grandes pueden servir como protección contra esas fallas, pero también traen aparejada una mayor latencia de audio. En el caso de las aplicaciones que requieren latencia baja, una transmisión de audio puede usar una función de devolución de llamada asíncrona para transferir datos hacia y desde tu app. AAudio ejecuta la devolución de llamada en un subproceso de mayor prioridad que tiene mejor rendimiento.
La función de devolución de llamada tiene el siguiente prototipo:
typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(
AAudioStream *stream,
void *userData,
void *audioData,
int32_t numFrames);
Usa la generación de transmisiones para registrar la devolución de llamada:
AAudioStreamBuilder_setDataCallback(builder, myCallback, myUserData);
En el caso más simple, la transmisión ejecuta periódicamente la función de devolución de llamada para obtener los datos del próximo pico de actividad.
La función de devolución de llamada no debe realizar tareas de lectura ni escritura en la transmisión que la invocó. Si la devolución de llamada pertenece a una transmisión de entrada, el código debe procesar los datos que se suministran en el búfer audioData (especificado como el tercer argumento). Si la devolución de llamada pertenece a una transmisión de salida, el código debe ubicar los datos en el búfer.
Por ejemplo, podrías usar una devolución de llamada para generar continuamente una salida de onda sinusoidal como la siguiente:
aaudio_data_callback_result_t myCallback(
AAudioStream *stream,
void *userData,
void *audioData,
int32_t numFrames) {
int64_t timeout = 0;
// Write samples directly into the audioData array.
generateSineWave(static_cast<float *>(audioData), numFrames);
return AAUDIO_CALLABCK_RESULT_CONTINUE;
}
Se puede procesar más de una transmisión con AAudio. Puedes usar una transmisión como principal y pasar punteros a otras transmisiones en los datos del usuario. Registra una devolución de llamada para la transmisión principal. Luego, utiliza E/S sin bloqueo en las demás transmisiones. A continuación, te mostramos un ejemplo de una devolución de llamada de ida y vuelta que transfiere una transmisión de entrada a una de salida. La transmisión de llamada principal es la de salida. La de entrada está incluida en los datos del usuario.
La devolución de llamada realiza una lectura sin bloqueo de la transmisión de entrada colocando los datos en el búfer de la transmisión de salida:
aaudio_data_callback_result_t myCallback(
AAudioStream *stream,
void *userData,
void *audioData,
int32_t numFrames) {
AAudioStream *inputStream = (AAudioStream *) userData;
int64_t timeout = 0;
aaudio_result_t result =
AAudioStream_read(inputStream, audioData, numFrames, timeout);
if (result == numFrames)
return AAUDIO_CALLABCK_RESULT_CONTINUE;
if (result >= 0) {
memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
return AAUDIO_CALLBACK_RESULT_CONTINUE;
}
return AAUDIO_CALLBACK_RESULT_STOP;
}
Ten en cuenta que, en este ejemplo, se asume que las transmisiones de entrada y salida poseen igual cantidad de canales, formato y tasa de muestreo. El formato de las transmisiones puede no coincidir; siempre y cuando el código realice las traducciones correctamente.
Cómo configurar el modo de rendimiento
Cada AAudioStream tiene un modo de rendimiento con un gran efecto sobre el comportamiento de la app. Existen tres modos:
AAUDIO_PERFORMANCE_MODE_NONE
es el modo predeterminado. Emite una transmisión básica que equilibra el ahorro de latencia y energía.AAUDIO_PERFORMANCE_MODE_LOW_LATENCY
utiliza búferes más pequeños y una ruta de acceso de datos optimizada para reducir la latencia.AAUDIO_PERFORMANCE_MODE_POWER_SAVING
utiliza búferes internos más grandes y una ruta de acceso de datos que intercambia latencia por menor energía.
Puedes llamar a setPerformanceMode() para seleccionar el modo de rendimiento y llamar a getPerformanceMode() a fin de descubrir el modo actual.
Si la latencia baja tiene prioridad por sobre el ahorro de energía en tu aplicación, utiliza AAUDIO_PERFORMANCE_MODE_LOW_LATENCY
.
Este modo es útil para las apps muy interactivas, como juegos o sintetizadores con teclado.
Si el ahorro de energía tiene prioridad por sobre la latencia baja en tu aplicación, usa AAUDIO_PERFORMANCE_MODE_POWER_SAVING
.
Este modo es el típico para apps que reproducen música ya generada, como es el caso de la transmisión de audio o los reproductores de archivos MIDI.
En la versión actual de AAudio, para lograr la menor latencia posible, debes utilizar el modo de rendimiento AAUDIO_PERFORMANCE_MODE_LOW_LATENCY
junto con una devolución de llamada de alta prioridad. Sigue este ejemplo:
// Create a stream builder
AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);
AAudioStreamBuilder_setDataCallback(streamBuilder, dataCallback, nullptr);
AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
// Use it to create the stream
AAudioStream *stream;
AAudioStreamBuilder_openStream(streamBuilder, &stream);
Seguridad del subproceso
La API de AAudio no es completamente segura para el subproceso. No puedes llamar a algunas funciones de AAudio conjuntamente desde más de un subproceso a la vez. Esto se debe a que AAudio evita utilizar exclusiones mutuas, que pueden provocar fallas en los subprocesos y evitarlos.
Como medida de seguridad, no llames a AAudioStream_waitForStateChange()
ni realices tareas de lectura ni escritura en la misma transmisión desde dos subprocesos diferentes. Del mismo modo, no cierres una transmisión en un subproceso mientras completas en ella tareas de lectura o escritura desde otro subproceso.
Las llamadas que devuelven configuraciones de transmisión, como AAudioStream_getSampleRate()
y AAudioStream_getChannelCount()
, son seguras para el subproceso.
Estas llamadas también son seguras para el subproceso:
AAudio_convert*ToText()
AAudio_createStreamBuilder()
AAudioStream_get*()
, excepto porAAudioStream_getTimestamp()
Errores conocidos
- La latencia de audio es alta para bloquear la función write() debido a que la versión de Android O DP2 no utiliza una pista RÁPIDA. Utiliza una devolución de llamada para obtener menor latencia.
Recursos adicionales
Para obtener más información, utiliza los siguientes recursos: