AAudio est une nouvelle API C Android introduite dans la version Android O. Elle est destinée aux applications audio haute performance qui nécessitent une faible latence. Les applications communiquent avec AAudio en lisant et en écrivant des données dans des flux.
Du fait de sa conception volontairement minimale, l'API AAudio n'exécute pas les fonctionnalités suivantes :
- Énumération des appareils audio
- Routage automatisé entre les points de terminaison audio
- E/S de fichiers
- Décodage de contenus audio compressés
- Présentation automatique de l'ensemble des entrées/flux dans un seul rappel
Premiers pas
Vous pouvez appeler AAudio à partir de code C++. Pour ajouter l'ensemble de fonctionnalités AAudio à votre application, incluez le fichier d'en-tête AAudio.h :
#include <aaudio/AAudio.h>
Flux audio
AAudio transfère les données audio entre votre application et les entrées et sorties audio de votre appareil Android. Votre application transmet des données en les lisant et en les écrivant dans des flux audio représentés par la structure AAudioStream. Ces appels de lecture/écriture peuvent être bloquants ou non.
Un flux est défini par les éléments suivants :
- Appareil audio jouant le rôle de source ou de récepteur des données du flux
- Mode de partage déterminant si un flux dispose d'un accès exclusif à un appareil audio, qui peut sinon être partagé par plusieurs flux
- Format des données audio du flux
Appareil audio
Chaque flux est associé à un seul appareil audio.
Un appareil audio est une interface matérielle ou un point de terminaison virtuel qui joue le rôle de source ou de récepteur d'un flux continu de données audionumériques. Ne confondez pas un appareil audio (micro intégré ou casque Bluetooth) avec l'appareil Android (téléphone ou montre) qui exécute votre application.
Vous pouvez utiliser la méthode AudioManager
getDevices()
pour détecter les appareils audio disponibles sur votre appareil Android. Cette méthode renvoie des informations sur le type
de chaque appareil.
Chaque appareil audio possède un identifiant unique sur l'appareil Android. Cet ID vous permet de lier un flux audio à un appareil audio spécifique. Toutefois, dans la plupart des cas, vous pouvez laisser AAudio choisir l'appareil principal par défaut au lieu d'en spécifier un vous-même.
L'appareil audio associé à un flux détermine si ce dernier est destiné à être utilisé comme entrée ou sortie. Un flux ne peut déplacer des données que dans une seule direction. Lorsque vous définissez un flux, vous définissez également sa direction. Lorsque vous ouvrez un flux, Android vérifie que l'appareil audio et la direction du flux correspondent.
Mode de partage
Un flux est associé à un mode de partage :
AAUDIO_SHARING_MODE_EXCLUSIVE
signifie que le flux dispose d'un accès exclusif à son appareil audio, qui ne peut donc être utilisé par aucun autre flux audio. Si l'appareil audio est déjà utilisé, il est possible que le flux ne dispose pas d'un accès exclusif. Les flux exclusifs présentent généralement une latence plus faible, mais ils sont également davantage susceptibles d'être déconnectés. Vous devez fermer les flux exclusifs dès que vous n'en avez plus besoin afin que les autres applications puissent accéder à l'appareil. Les flux exclusifs offrent la latence la plus faible possible.AAUDIO_SHARING_MODE_SHARED
permet à AAudio de réaliser un mixage audio. AAudio mélange tous les flux partagés attribués au même appareil.
Vous pouvez définir explicitement le mode de partage lorsque vous créez un flux. Par défaut, le mode de partage est SHARED
.
Format audio
Les données transmises via un flux possèdent les attributs audionumériques habituels, à savoir :
- Format d'échantillon de données
- Nombre de canaux (échantillons par trame)
- Taux d'échantillonnage
AAudio autorise les formats d'échantillon suivants :
aaudio_format_t | Type de données C | Remarques |
---|---|---|
AAUDIO_FORMAT_PCM_I16 | int16_t | Échantillons 16 bits courants, format Q0.15 |
AAUDIO_FORMAT_PCM_FLOAT | float | -1,0 à +1,0 |
AAUDIO_FORMAT_PCM_I24_PACKED | uint8_t par groupes de 3 | Échantillons empaquetés 24 bits, format Q0.23 |
AAUDIO_FORMAT_PCM_I32 | int32_t | Échantillons 32 bits courants, format Q0.31 |
AAUDIO_FORMAT_IEC61937 | uint8_t | Audio compressé encapsulé en IEC61937 pour le passthrough HDMI ou S/PDIF |
Si vous demandez un format d'échantillon spécifique, le flux l'utilise même s'il n'est pas optimal pour l'appareil. Si vous n'en spécifiez aucun, AAudio choisit un format optimal. Une fois le flux ouvert, vous devez interroger le format d'échantillon de données, puis convertir les données si nécessaire, comme dans cet exemple :
aaudio_format_t dataFormat = AAudioStream_getDataFormat(stream);
//... later
if (dataFormat == AAUDIO_FORMAT_PCM_I16) {
convertFloatToPcm16(...)
}
Créer un flux audio
La bibliothèque AAudio suit un schéma de conception builder (outil de création de flux) et fournit AAudioStreamBuilder.
- Créez un AAudioStreamBuilder :
AAudioStreamBuilder *builder; aaudio_result_t result = AAudio_createStreamBuilder(&builder);
- Définissez la configuration de flux audio dans le builder à l'aide des fonctions correspondant aux paramètres de flux. Les fonctions set facultatives suivantes sont disponibles :
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);
Notez que ces méthodes ne signalent pas les erreurs, telles qu'une constante non définie ou une valeur hors limites.
Si vous ne spécifiez pas l'ID de l'appareil, la valeur par défaut correspond au périphérique de sortie principal. Si vous ne spécifiez pas la direction du flux, la valeur par défaut est un flux de sortie. Pour tous les autres paramètres, vous pouvez définir une valeur de manière explicite ou laisser le système attribuer la valeur optimale en ne spécifiant pas le paramètre ou en le définissant sur
AAUDIO_UNSPECIFIED
.Par mesure de précaution, vérifiez l'état du flux audio après sa création, comme expliqué à l'étape 4 ci-dessous.
- Une fois l'AAudioStreamBuilder configuré, utilisez-le pour créer un flux :
AAudioStream *stream; result = AAudioStreamBuilder_openStream(builder, &stream);
- Après avoir créé le flux, vérifiez sa configuration. Si vous avez spécifié un format d'échantillon, un taux d'échantillonnage ou un nombre d'échantillons par trame, ils ne sont pas modifiés. Si vous avez spécifié le mode de partage ou la capacité de la mémoire tampon, ils peuvent changer en fonction des fonctionnalités de l'appareil audio du flux et de l'appareil Android sur lequel il s'exécute. Pour une programmation défensive de qualité, vous devez vérifier la configuration du flux avant de l'utiliser. Des fonctions permettent de récupérer le paramètre de flux correspondant à chaque paramètre du builder :
- Vous pouvez enregistrer le builder et le réutiliser ultérieurement pour créer d'autres flux. Si vous ne souhaitez plus l'utiliser, vous devez le supprimer.
AAudioStreamBuilder_delete(builder);
Utiliser un flux audio
Transitions d'état
Un flux AAudio se trouve généralement dans l'un des cinq états stables suivants (l'état d'erreur "Déconnecté" est décrit à la fin de cette section) :
- Ouvert
- Démarré
- Suspendu
- Vidé
- Arrêté
Les données ne transitent par un flux que s'il est à l'état "Démarré". Pour faire passer un flux d'un état à un autre, utilisez l'une des fonctions qui demandent une transition d'état :
aaudio_result_t result;
result = AAudioStream_requestStart(stream);
result = AAudioStream_requestStop(stream);
result = AAudioStream_requestPause(stream);
result = AAudioStream_requestFlush(stream);
Notez que vous ne pouvez demander la suspension ou le vidage que pour un flux de sortie :
Ces fonctions sont asynchrones et le changement d'état ne s'effectue pas immédiatement. Lorsque vous demandez un changement d'état, le flux passe à l'un des états transitoires correspondants :
- Démarrage
- Suspension
- Vidage
- Arrêt
- Fermeture
Le diagramme ci-dessous présente les états stables sous forme de rectangles à coins arrondis et les états transitoires sous forme de rectangles en pointillés.
Bien que cela ne se voie pas sur le diagramme, il est possible d'appeler close()
depuis n'importe quel état.
AAudio ne fournit pas de rappels pour vous alerter en cas de changement d'état. Une fonction spéciale, AAudioStream_waitForStateChange(stream, inputState, nextState, timeout)
, peut être utilisée pour attendre un changement d'état.
La fonction ne détecte pas à elle seule un changement d'état et elle n'attend pas un état particulier. Elle attend jusqu'à ce que l'état actuel soit différent de l'état inputState
, que vous spécifiez.
Par exemple, suite à une demande de suspension, un flux doit immédiatement passer à l'état transitoire "Suspension", puis passer au bout d'un certain temps à l'état "Suspendu", bien que cela ne soit pas garanti.
Comme vous ne pouvez pas attendre l'état "Suspendu", utilisez waitForStateChange()
pour attendre tout état autre que "Suspension". Voici comment procéder :
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 le flux n'est pas à l'état "Suspension" (état inputState
, que nous supposons correspondre à l'état actuel au moment de l'appel), la fonction retourne immédiatement au programme appelant. S'il est à l'état "Suspension", la fonction se bloque jusqu'à ce que cet état change ou que le délai avant expiration soit écoulé. Lors du retour de la fonction, le paramètre nextState
indique l'état actuel du flux.
Vous pouvez utiliser cette même technique après l'appel d'une requête de démarrage, d'arrêt ou de vidage, en utilisant l'état transitoire correspondant comme état inputState. N'appelez pas waitForStateChange()
après avoir appelé AAudioStream_close()
, car le flux est supprimé dès qu'il est fermé. N'appelez pas AAudioStream_close()
lorsque waitForStateChange()
s'exécute dans un autre thread.
Lire et écrire dans un flux audio
Une fois le flux démarré, il existe deux façons de traiter les données qu'il contient :
- Utiliser un rappel à priorité élevée
- Utiliser les fonctions
AAudioStream_read(stream, buffer, numFrames, timeoutNanos)
etAAudioStream_write(stream, buffer, numFrames, timeoutNanos)
pour lire ou écrire le flux
Pour une lecture ou une écriture bloquante qui transfère le nombre de trames spécifié, définissez timeoutNanos sur une valeur supérieure à zéro. Pour un appel non bloquant, définissez timeoutNanos sur zéro. Dans ce cas, le résultat correspond au nombre réel de trames transférées.
Lorsque vous lisez une entrée, vous devez vérifier que le nombre de trames lues est correct. Si ce n'est pas le cas, le tampon contient peut-être des données inconnues susceptibles de provoquer un problème audio. Vous pouvez remplir le tampon avec des zéros pour créer une interruption silencieuse :
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);
}
Vous pouvez préparer le tampon du flux avant de démarrer ce dernier en écrivant des données ou en y insérant du silence. Cette opération doit être effectuée dans un appel non bloquant avec timeoutNanos défini sur zéro.
Les données du tampon doivent correspondre au format de données renvoyé par AAudioStream_getDataFormat()
.
Fermer un flux audio
Lorsque vous avez fini d'utiliser un flux, fermez-le :
AAudioStream_close(stream);
Une fois que vous avez fermé un flux, vous ne pouvez plus l'utiliser avec aucune fonction basée sur les flux AAudio.
Flux audio déconnecté
Un flux audio peut être déconnecté à tout moment si l'un des événements suivants se produit :
- L'appareil audio associé n'est plus connecté (par exemple, lorsqu'un casque est débranché).
- Une erreur interne se produit.
- L'appareil audio n'est plus l'appareil audio principal.
Lorsqu'un flux est déconnecté, il est à l'état "Déconnecté" et toute tentative d'exécution de AAudioStream_write() ou d'autres fonctions renvoie une erreur. Vous devez toujours arrêter et fermer un flux déconnecté, quel que soit le code d'erreur.
Si vous utilisez un rappel de données (par opposition à l'une des méthodes de lecture/écriture directes), vous ne recevez pas de code de retour lorsque le flux est déconnecté. Pour être informé de cette déconnexion, rédigez une fonction AAudioStream_errorCallback et enregistrez-la à l'aide de AAudioStreamBuilder_setErrorCallback().
Si vous êtes informé de la déconnexion dans un thread de rappel d'erreur, l'arrêt et la fermeture du flux doivent être effectués à partir d'un autre thread. Sinon, un interblocage peut se produire.
Notez que si vous ouvrez un nouveau flux, ses paramètres peuvent être différents de ceux du flux d'origine (par exemple, 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.
}
Optimiser les performances
Vous pouvez optimiser les performances d'une application audio en ajustant ses tampons internes et en utilisant des threads spéciaux à priorité élevée.
Régler les tampons pour réduire la latence
AAudio transmet les données depuis et vers les tampons internes qu'il gère, à savoir un pour chaque appareil audio.
La capacité du tampon correspond à la quantité totale de données qu'il peut contenir. Vous pouvez la définir en appelant AAudioStreamBuilder_setBufferCapacityInFrames()
. Cette méthode limite la capacité que vous pouvez allouer à la valeur maximale autorisée par l'appareil. Utilisez AAudioStream_getBufferCapacityInFrames()
pour vérifier la capacité réelle du tampon.
Une application n'a pas besoin d'utiliser toute la capacité d'un tampon. AAudio remplit un tampon jusqu'à une taille maximale que vous pouvez définir. La taille d'un tampon ne peut pas être supérieure à sa capacité. Elle lui est souvent inférieure. En contrôlant la taille de la mémoire tampon, vous déterminez le nombre de rafales nécessaires pour la remplir et contrôlez ainsi la latence. Utilisez les méthodes AAudioStreamBuilder_setBufferSizeInFrames()
et AAudioStreamBuilder_getBufferSizeInFrames()
pour connaître et définir la taille de la mémoire tampon.
Lorsqu'une application lit du contenu audio, elle écrit dans un tampon et se bloque jusqu'à ce que l'écriture soit terminée. AAudio lit le contenu du tampon en rafales distinctes. Chaque rafale contient plusieurs trames audio et a généralement une taille inférieure à celle du tampon en cours de lecture. Le système contrôle la taille et le débit des rafales. Ces propriétés sont généralement déterminées par le circuit de l'appareil audio. Bien que vous ne puissiez pas modifier la taille ni le débit des rafales, vous pouvez définir la taille du tampon interne en fonction du nombre de rafales qu'il contient. En général, vous obtenez la latence la plus faible si la taille de la mémoire tampon de votre AAudioStream est un multiple de la taille de rafale indiquée.
Pour optimiser la taille de la mémoire tampon, vous pouvez commencer par utiliser un tampon de grande taille et le réduire progressivement jusqu'à ce qu'une sous-utilisation soit constatée, puis le réaugmenter. Vous pouvez également commencer par une petite taille de mémoire tampon. Si cela entraîne une sous-utilisation, augmentez la taille jusqu'à ce que les données de sortie soient à nouveau transmises correctement.
Ce processus peut s'effectuer très rapidement, parfois avant l'émission du premier son. Vous pouvez d'abord procéder au dimensionnement initial de la mémoire tampon en utilisant du silence pour que l'utilisateur n'entende aucun problème audio. Les performances du système peuvent changer au fil du temps (par exemple, l'utilisateur peut désactiver le mode Avion). Comme le réglage du tampon ne génère qu'une surcharge minimale, votre application peut l'effectuer en continu pendant qu'elle lit ou écrit des données dans un flux.
Voici un exemple de boucle d'optimisation de tampon :
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);
}
}
}
Cette technique ne présente aucun avantage lorsqu'il s'agit d'optimiser la taille de la mémoire tampon d'un flux d'entrée. Les flux d'entrée s'exécutent aussi vite que possible, en essayant de réduire au minimum la quantité de données en mémoire tampon, puis en la remplissant lorsque l'application est préemptée.
Utiliser un rappel à priorité élevée
Si votre application lit ou écrit des données audio à partir d'un thread ordinaire, elle peut être préemptée ou présenter une gigue temporelle, ce qui peut provoquer des problèmes audio. L'utilisation de tampons plus volumineux peut permettre d'éviter ces problèmes, mais également entraîner une latence audio plus importante. Pour les applications nécessitant une faible latence, un flux audio peut utiliser une fonction de rappel asynchrone pour transférer des données vers et depuis votre application. AAudio exécute le rappel dans un thread de priorité supérieure qui offre de meilleures performances.
La fonction de rappel correspond au prototype suivant :
typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(
AAudioStream *stream,
void *userData,
void *audioData,
int32_t numFrames);
Utilisez l'outil de création de flux pour enregistrer le rappel :
AAudioStreamBuilder_setDataCallback(builder, myCallback, myUserData);
Dans le cas le plus simple, le flux exécute régulièrement la fonction de rappel pour acquérir les données en vue de la prochaine rafale.
La fonction de rappel ne doit pas effectuer de lecture ni d'écriture sur le flux qui l'a appelée. Si le rappel appartient à un flux d'entrée, votre code doit traiter les données fournies dans le tampon audioData (spécifié dans le troisième argument). Si le rappel appartient à un flux de sortie, votre code doit placer les données dans le tampon.
Par exemple, vous pouvez utiliser un rappel pour générer en continu une sortie d'onde sinusoïdale comme suit :
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;
}
Il est possible de traiter plusieurs flux à l'aide d'AAudio. Vous pouvez utiliser un flux comme maître et transmettre des pointeurs vers d'autres flux dans les données utilisateur. Enregistrez un rappel pour le flux maître, puis utilisez des E/S non bloquantes sur les autres flux. Voici un exemple de rappel aller-retour qui transmet un flux d'entrée à un flux de sortie. Le flux appelant maître est le flux de sortie. Le flux d'entrée est inclus dans les données utilisateur.
Le rappel effectue une lecture non bloquante du flux d'entrée et place les données dans le tampon du flux de sortie :
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;
}
Notez que, dans cet exemple, nous supposons que les flux d'entrée et de sortie ont le même nombre de canaux, le même format et le même taux d'échantillonnage. Les flux peuvent avoir des formats différents, à condition que le code gère correctement les conversions.
Définir le mode Performances
Chaque AAudioStream dispose d'un mode de performance qui a un effet important sur le comportement de votre application. Il existe trois modes :
AAUDIO_PERFORMANCE_MODE_NONE
est le mode par défaut. Il utilise un flux de base qui équilibre la latence et les économies d'énergie.AAUDIO_PERFORMANCE_MODE_LOW_LATENCY
utilise des tampons plus petits et un chemin de données optimisé pour réduire la latence.AAUDIO_PERFORMANCE_MODE_POWER_SAVING
utilise des tampons internes plus volumineux et un chemin de données qui favorise les économies d'énergie aux dépens de la latence.
Vous pouvez sélectionner le mode Performances en appelant setPerformanceMode(), et détecter le mode actuel en appelant getPerformanceMode().
Si une faible latence est plus importante que les économies d'énergie pour votre application, utilisez AAUDIO_PERFORMANCE_MODE_LOW_LATENCY
.
Ce mode est utile pour les applications hautement interactives, telles que les jeux ou les synthétiseurs.
Si les économies d'énergie sont plus importantes qu'une faible latence pour votre application, utilisez AAUDIO_PERFORMANCE_MODE_POWER_SAVING
.
C'est généralement le cas des applications qui diffusent de la musique générée préalablement, par exemple les appareils de streaming audio ou les lecteurs de fichiers MIDI.
Pour obtenir la latence la plus faible possible avec la version actuelle d'AAudio, vous devez utiliser le mode Performances AAUDIO_PERFORMANCE_MODE_LOW_LATENCY
, ainsi qu'un rappel à priorité élevée. Suivez cet exemple :
// 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);
Thread safety
L'API AAudio n'est pas complètement thread-safe. Vous ne pouvez pas appeler simultanément certaines fonctions AAudio à partir de plusieurs threads. En effet, AAudio évite d'utiliser des exclusions mutuelles, ce qui peut entraîner une préemption des threads et d'autres problèmes.
Pour plus de sécurité, n'appelez pas AAudioStream_waitForStateChange()
et ne lisez ni n'écrivez pas dans le même flux à partir de deux threads différents. De même, ne fermez pas un flux dans un thread alors que vous le lisez ou y écrivez dans un autre.
Les appels qui renvoient des paramètres de flux, tels que AAudioStream_getSampleRate()
et AAudioStream_getChannelCount()
, sont thread-safe.
Les appels suivants sont également thread-safe :
AAudio_convert*ToText()
AAudio_createStreamBuilder()
AAudioStream_get*()
, saufAAudioStream_getTimestamp()
Problèmes connus
- La latence audio est élevée pour les write() bloquants, car la version Android O DP2 n'utilise pas de piste rapide. Utilisez un rappel pour réduire la latence.
Ressources supplémentaires
Pour en savoir plus, consultez les ressources suivantes :
Référence de l'API
Ateliers de programmation
Vidéos
- Best Practices for Android Audio (Google I/O '17) (Bonnes pratiques pour Android Audio (Google I/O 2017)