AAudio è una nuova API C per Android introdotta nella release Android O. È progettato per applicazioni audio ad alte prestazioni che richiedono una bassa latenza. Le app comunicano con AAudio leggendo e scrivendo dati negli stream.
L'API AAudio è minima per progettazione, non esegue queste funzioni:
- Enumerazione dei dispositivi audio
- Routing automatico tra endpoint audio
- I/O file
- Decodifica dell'audio compresso
- Presentazione automatica di tutti gli input/stream in un unico callback.
Per iniziare
Puoi chiamare AAudio dal codice C++. Per aggiungere il set di funzionalità AAudio alla tua app, includi il file di intestazione AAudio.h:
#include <aaudio/AAudio.h>
Stream audio
AAudio sposta i dati audio tra l'app e le entrate e le uscite audio sul dispositivo Android. L'app trasmette i dati in entrata e in uscita leggendo e scrivendo in stream audio, rappresentati dalla struttura AAudioStream. Le chiamate di lettura/scrittura possono essere bloccanti o non bloccanti.
Uno stream è definito da quanto segue:
- Il dispositivo audio che rappresenta la sorgente o la destinazione dei dati nello stream.
- La modalità di condivisione che determina se uno stream ha accesso esclusivo a un dispositivo audio che altrimenti potrebbe essere condiviso tra più stream.
- Il formato dei dati audio nello stream.
Dispositivo audio
Ogni stream è collegato a un singolo dispositivo audio.
Un dispositivo audio è un'interfaccia hardware o un endpoint virtuale che funge da origine o destinazione per uno stream continuo di dati audio digitali. Non confondere un dispositivo audio (un microfono integrato o cuffie Bluetooth) con il dispositivo Android (lo smartphone o lo smartwatch) su cui è in esecuzione la tua app.
Puoi utilizzare il metodo AudioManager
getDevices()
per scoprire i dispositivi audio disponibili sul tuo dispositivo Android. Il metodo restituisce informazioni sul type
di ciascun dispositivo.
Ogni dispositivo audio ha un ID univoco sul dispositivo Android. Puoi utilizzare l'ID per associare uno stream audio a un dispositivo audio specifico. Tuttavia, nella maggior parte dei casi puoi lasciare che sia AAudio a scegliere il dispositivo principale predefinito anziché specificarne uno tu.
Il dispositivo audio collegato a uno stream determina se lo stream è per l'input o l'output. Uno stream può spostare i dati in una sola direzione. Quando definisci uno stream, ne imposti anche la direzione. Quando apri uno stream, Android verifica che il dispositivo audio e la direzione dello stream siano in accordo.
Modalità di condivisione
Uno stream ha una modalità di condivisione:
AAUDIO_SHARING_MODE_EXCLUSIVE
indica che lo stream ha accesso esclusivo al dispositivo audio; il dispositivo non può essere utilizzato da nessun altro stream audio. Se il dispositivo audio è già in uso, lo stream potrebbe non avere accesso esclusivo. Gli stream esclusivi hanno una latenza inferiore, ma sono anche più soggetti a disconnessione. Devi chiudere gli stream esclusivi non appena non ne hai più bisogno, in modo che altre app possano accedere al dispositivo. Gli stream esclusivi offrono la latenza più bassa possibile.AAUDIO_SHARING_MODE_SHARED
consente ad AAudio di mixare l'audio. AAudio mischia tutti gli stream condivisi assegnati allo stesso dispositivo.
Puoi impostare la modalità di condivisione in modo esplicito quando crei uno stream. Per impostazione predefinita, la modalità di condivisione è SHARED
.
Formato audio
I dati trasmessi tramite uno stream hanno i soliti attributi audio digitali. Di seguito sono riportati i requisiti:
- Formato dei dati di esempio
- Numero di canali (campioni per frame)
- Frequenza di campionamento
AAudio consente i seguenti formati di sample:
aaudio_format_t | Tipo di dati C | Note |
---|---|---|
AAUDIO_FORMAT_PCM_I16 | int16_t | Campioni comuni a 16 bit, formato Q0.15 |
AAUDIO_FORMAT_PCM_FLOAT | galleggiare | Da -1,0 a +1,0 |
AAUDIO_FORMAT_PCM_I24_PACKED | uint8_t in gruppi di 3 | Campioni 24 bit pacchettizzati, formato Q0.23 |
AAUDIO_FORMAT_PCM_I32 | int32_t | Campioni comuni a 32 bit, formato Q0.31 |
AAUDIO_FORMAT_IEC61937 | uint8_t | Audio compresso con wrapping IEC61937 per il passthrough HDMI o S/PDIF |
Se richiedi un formato di sample specifico, lo stream lo utilizzerà anche se non è ottimale per il dispositivo. Se non specifichi un formato di sample, AAudio ne sceglierà uno ottimale. Dopo aver aperto lo stream, devi eseguire una query sul formato dei dati di esempio e poi convertire i dati, se necessario, come in questo esempio:
aaudio_format_t dataFormat = AAudioStream_getDataFormat(stream);
//... later
if (dataFormat == AAUDIO_FORMAT_PCM_I16) {
convertFloatToPcm16(...)
}
Creazione di uno stream audio
La libreria AAudio segue un pattern di progettazione del builder e fornisce AAudioStreamBuilder.
- Crea un AAudioStreamBuilder:
AAudioStreamBuilder *builder; aaudio_result_t result = AAudio_createStreamBuilder(&builder);
- Imposta la configurazione dello stream audio nel generatore, utilizzando le funzioni del generatore corrispondenti ai parametri dello stream. Sono disponibili le seguenti funzioni di impostazione facoltative:
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);
Tieni presente che questi metodi non segnalano errori, ad esempio una costante non definita o un valore fuori intervallo.
Se non specifichi il valore deviceId, il valore predefinito è il dispositivo di output principale. Se non specifichi la direzione dello stream, il valore predefinito è uno stream di output. Per tutti gli altri parametri, puoi impostare esplicitamente un valore o lasciare che sia il sistema a assegnare il valore ottimale non specificando il parametro o impostandolo su
AAUDIO_UNSPECIFIED
.Per sicurezza, controlla lo stato dello stream audio dopo averlo creato, come spiegato nel passaggio 4 di seguito.
- Una volta configurato, utilizza AAudioStreamBuilder per creare uno stream:
AAudioStream *stream; result = AAudioStreamBuilder_openStream(builder, &stream);
- Dopo aver creato lo stream, verificane la configurazione. Se hai specificato il formato di sample, la frequenza di campionamento o i sample per frame, questi non cambieranno. Se hai specificato la modalità di condivisione o la capacità del buffer, queste potrebbero cambiare in base alle funzionalità del dispositivo audio dello stream e del dispositivo Android su cui è in esecuzione. Come buona prassi di programmazione difensiva, devi controllare la configurazione dello stream prima di utilizzarlo. Esistono funzioni per recuperare l'impostazione dello stream corrispondente a ogni impostazione del generatore:
- Puoi salvare il generatore e riutilizzarlo in futuro per creare altri stream. Tuttavia, se non intendi utilizzarlo più, devi eliminarlo.
AAudioStreamBuilder_delete(builder);
Utilizzare uno stream audio
Transizioni di stato
In genere, uno stream AAudio si trova in uno dei cinque stati stabili (lo stato di errore Disconnesso è descritto alla fine di questa sezione):
- Apri
- Avviato
- In pausa
- Faccina rossa in viso con occhi spalancati
- Interrotto
I dati vengono trasferiti in uno stream solo quando lo stream è nello stato Avviato. Per spostare uno stream tra stati, utilizza una delle funzioni che richiedono una transizione di stato:
aaudio_result_t result;
result = AAudioStream_requestStart(stream);
result = AAudioStream_requestStop(stream);
result = AAudioStream_requestPause(stream);
result = AAudioStream_requestFlush(stream);
Tieni presente che puoi richiedere la messa in pausa o lo svuotamento di uno stream di output solo:
Queste funzioni sono asincrone e la modifica dello stato non avviene immediatamente. Quando richiedi una modifica dello stato, lo stream passa a uno degli stati di transizione corrispondenti:
- Avvio in corso…
- Messa in pausa
- Scarico
- Interruzione in corso
- Chiusura
Il diagramma di stato riportato di seguito mostra gli stati stabili come rettangoli arrotondati e gli stati transitori come rettangoli tratteggiati.
Anche se non è mostrato, puoi chiamare close()
da qualsiasi stato
AAudio non fornisce callback per avvisarti delle modifiche dello stato. Una funzione speciale, AAudioStream_waitForStateChange(stream, inputState, nextState, timeout)
, può essere utilizzata per attendere una modifica dello stato.
La funzione non rileva autonomamente una modifica dello stato e non attende un
stato specifico. Attende fino a quando lo stato corrente
è diverso da inputState
, che hai specificato.
Ad esempio, dopo aver richiesto la messa in pausa, uno stream dovrebbe entrare immediatamente nello stato transitorio Pausa e arrivare in un secondo momento allo stato In pausa, anche se non è garantito che ciò accada.
Poiché non puoi attendere lo stato In pausa, utilizza waitForStateChange()
per attendere qualsiasi stato diverso da Pausa. Ecco come fare:
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);
Se lo stato dello stream non è Pausa (inputState
, che abbiamo assunto essere lo stato corrente al momento della chiamata), la funzione restituisce immediatamente. In caso contrario, si blocca finché lo stato non è più In pausa o il timeout non scade. Quando la funzione restituisce, il parametro nextState
mostra lo stato corrente dello stream.
Puoi utilizzare la stessa tecnica dopo aver chiamato start, stop o flush della richiesta, utilizzando lo stato transitorio corrispondente come inputState. Non chiamare
waitForStateChange()
dopo aver chiamato AAudioStream_close()
, poiché lo stream
verrà eliminato non appena si chiude. Inoltre, non chiamare AAudioStream_close()
mentre waitForStateChange()
è in esecuzione in un altro thread.
Lettura e scrittura in uno stream audio
Esistono due modi per elaborare i dati in uno stream dopo l'avvio:
- Utilizza un richiamo con priorità elevata.
- Utilizza le funzioni
AAudioStream_read(stream, buffer, numFrames, timeoutNanos)
eAAudioStream_write(stream, buffer, numFrames, timeoutNanos)
. per leggere o scrivere lo stream.
Per una lettura o scrittura bloccante che trasferisce il numero specificato di frame, imposta timeoutNanos su un valore maggiore di zero. Per una chiamata non bloccante, imposta timeoutNanos su zero. In questo caso, il risultato è il numero effettivo di frame trasferiti.
Quando leggi l'input, devi verificare che sia stato letto il numero corretto di frame. In caso contrario, il buffer potrebbe contenere dati sconosciuti che potrebbero causare un glitch audio. Puoi riempire il buffer con zeri per creare un dropout silenzioso:
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);
}
Puoi inizializzare il buffer dello stream prima di avviarlo scrivendo dati o silenzio. Questa operazione deve essere eseguita in una chiamata non bloccante con timeoutNanos impostato su zero.
I dati nel buffer devono corrispondere al formato dei dati restituito da AAudioStream_getDataFormat()
.
Chiusura di uno stream audio
Quando hai finito di utilizzare uno stream, chiudilo:
AAudioStream_close(stream);
Dopo aver chiuso uno stream, non puoi utilizzare il relativo puntatore con alcuna funzione basata su stream AAudio.
La chiusura di uno stream non è sicura per i thread. NON provare a chiudere uno stream in un thread mentre lo utilizzi in un altro. Se utilizzi più thread, questi devono essere sincronizzati con attenzione. Ti consigliamo di inserire tutto il codice di gestione dello stream in un unico thread e poi di inviargli i comandi utilizzando una coda atomica.
Stream audio disconnesso
Uno stream audio può essere disconnesso in qualsiasi momento se si verifica uno di questi eventi:
- Il dispositivo audio associato non è più connesso (ad esempio quando le cuffie sono scollegate).
- Si verifica un errore interno.
- Un dispositivo audio non è più il dispositivo audio principale.
Quando uno stream è disconnesso, ha lo stato "Disconnesso" e qualsiasi tentativo di eseguire AAudioStream_write() o altre funzioni restituirà un errore. Devi sempre interrompere e chiudere uno stream disconnesso, indipendentemente dal codice di errore.
Se utilizzi un callback di dati (anziché uno dei metodi di lettura/scrittura diretti), non riceverai alcun codice di ritorno quando lo stream viene disconnesso. Per ricevere una notifica quando si verifica questo problema, scrivi una funzione AAudioStream_errorCallback e registrala utilizzando AAudioStreamBuilder_setErrorCallback().
Se ricevi una notifica della disconnessione in un thread di callback di errore, l'interruzione e la chiusura dello stream devono essere eseguite da un altro thread. In caso contrario, potresti riscontrare un deadlock.
Tieni presente che se apri un nuovo stream, le impostazioni potrebbero essere diverse da quelle dello stream originale (ad esempio 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.
}
Ottimizzazione delle prestazioni
Puoi ottimizzare il rendimento di un'applicazione audio regolando i relativi buffer interni e utilizzando thread speciali con priorità elevata.
Ottimizzazione dei buffer per ridurre al minimo la latenza
AAudio passa i dati all'interno e all'esterno dei buffer interni che gestisce, uno per ogni dispositivo audio.
La capacità del buffer è la quantità totale di dati che può contenere. Puoi chiamare
AAudioStreamBuilder_setBufferCapacityInFrames()
per impostare la capacità. Il metodo limita la capacità che puoi allocare al valore massimo consentito dal dispositivo. Utilizza
AAudioStream_getBufferCapacityInFrames()
per verificare la capacità effettiva del buffer.
Un'app non deve utilizzare l'intera capacità di un buffer. AAudio riempie un buffer fino a una dimensione che puoi impostare. Le dimensioni di un buffer non possono essere superiori alla sua capacità e spesso sono inferiori. Controllando la dimensione del buffer, puoi determinare il numero di picchi necessari per riempirlo e quindi controllare la latenza. Utilizza i metodi AAudioStreamBuilder_setBufferSizeInFrames()
e
AAudioStreamBuilder_getBufferSizeInFrames()
per gestire le dimensioni del buffer.
Quando un'applicazione riproduce l'audio, scrive in un buffer e si blocca fino al completamento della scrittura. AAudio legge dal buffer in burst discreti. Ogni burst contiene più frame audio ed è in genere più piccolo delle dimensioni del buffer in lettura. Il sistema controlla la dimensione e la frequenza degli burst, che in genere sono dettate dal circuito del dispositivo audio. Sebbene non sia possibile modificare le dimensioni di un'esplosione o la frequenza di esplosioni, puoi impostare le dimensioni del buffer interno in base al numero di esplosioni che contiene. In genere, ottieni la latenza più bassa se le dimensioni del buffer di AAudioStream sono un multiplo delle dimensioni dell'esplosione registrate.
Un modo per ottimizzare la dimensione del buffer è iniziare con un buffer grande e ridurlo gradualmente fino a quando non iniziano gli underrun, quindi aumentarlo di nuovo. In alternativa, puoi iniziare con una dimensione del buffer ridotta e, se si verificano sottocarichi, aumentare la dimensione del buffer fino a quando l'output non scorre di nuovo correttamente.
Questo processo può avvenire molto rapidamente, eventualmente prima che l'utente riproduca il primo suono. Ti consigliamo di eseguire prima la definizione delle dimensioni iniziali del buffer utilizzando il silenzio, in modo che l'utente non senta glitch audio. Il rendimento del sistema può cambiare nel tempo (ad esempio, l'utente potrebbe disattivare la modalità aereo). Poiché la regolazione del buffer aggiunge un overhead molto ridotto, la tua app può eseguirla continuamente mentre legge o scrive dati in uno stream.
Ecco un esempio di un ciclo di ottimizzazione del buffer:
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);
}
}
}
Non è vantaggioso utilizzare questa tecnica per ottimizzare le dimensioni del buffer per uno stream di input. Gli stream di input vengono eseguiti il più rapidamente possibile, cercando di mantenere al minimo la quantità di dati in buffer e poi riempiendosi quando l'app viene anticipata.
Utilizzo di un callback ad alta priorità
Se la tua app legge o scrive dati audio da un thread normale, potrebbe essere interrotta o presentare jitter nei tempi. Ciò può causare glitch audio. L'utilizzo di buffer più grandi potrebbe proteggere da questi problemi, ma un buffer di grandi dimensioni introduce anche una latenza audio più lunga. Per le applicazioni che richiedono una bassa latenza, uno stream audio può utilizzare una funzione di callback asincrona per trasferire dati da e verso l'app. AAudio esegue il callback in un thread con priorità più elevata e prestazioni migliori.
La funzione di callback ha questo prototipo:
typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(
AAudioStream *stream,
void *userData,
void *audioData,
int32_t numFrames);
Utilizza la creazione di stream per registrare il callback:
AAudioStreamBuilder_setDataCallback(builder, myCallback, myUserData);
Nel caso più semplice, lo stream esegue periodicamente la funzione di callback per acquisire i dati per il successivo picco.
La funzione di callback non deve eseguire operazioni di lettura o scrittura sullo stream che lo ha invocato. Se il callback appartiene a uno stream di input, il codice deve elaborare i dati forniti nel buffer audioData (specificato come terzo argomento). Se il callback appartiene a uno stream di output, il codice deve inserire i dati nel buffer.
Ad esempio, puoi utilizzare un callback per generare continuamente un output di un'onda sinusoidale come questa:
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;
}
È possibile elaborare più di uno stream utilizzando AAudio. Puoi utilizzare uno stream come principale e passare i puntatori ad altri stream nei dati utente. Registra un callback per lo stream principale. Poi utilizza l'I/O non bloccante negli altri stream. Ecco un esempio di chiamata di callback di andata e ritorno che passa uno stream di input a uno stream di output. Lo stream di chiamate principale è lo stream di output. Lo stream di input è incluso nei dati utente.
Il callback esegue una lettura non bloccante dallo stream di input inserendo i dati nel buffer dello stream di output:
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;
}
Tieni presente che in questo esempio si presume che gli stream di input e di output abbiano lo stesso numero di canali, formato e frequenza di campionamento. Il formato degli stream può non corrispondere, purché il codice gestisca correttamente le traduzioni.
Impostazione della modalità Prestazioni
Ogni AAudioStream ha una modalità di prestazioni che influisce notevolmente sul comportamento dell'app. Esistono tre modalità:
AAUDIO_PERFORMANCE_MODE_NONE
è la modalità predefinita. Utilizza uno stream di base che bilancia latenza e risparmio energetico.AAUDIO_PERFORMANCE_MODE_LOW_LATENCY
utilizza buffer più piccoli e un percorso dati ottimizzato per ridurre la latenza.AAUDIO_PERFORMANCE_MODE_POWER_SAVING
utilizza buffer interni più grandi e un percorso dati che riduce la latenza a fronte di una minore potenza.
Puoi selezionare la modalità di rendimento chiamando setPerformanceMode() e scoprire la modalità corrente chiamando getPerformanceMode().
Se nella tua applicazione la latenza bassa è più importante del risparmio energetico, utilizza AAUDIO_PERFORMANCE_MODE_LOW_LATENCY
.
Questa opzione è utile per le app molto interattive, come giochi o sintetizzatori con tastiera.
Se il risparmio energetico è più importante della latenza ridotta nella tua applicazione, utilizza AAUDIO_PERFORMANCE_MODE_POWER_SAVING
.
Questo è tipico delle app che riproducono musica generata in precedenza, ad esempio lettori di file MIDI o audio in streaming.
Nella versione corrente di AAudio, per ottenere la latenza più bassa possibile devi utilizzare la modalità di prestazioni AAUDIO_PERFORMANCE_MODE_LOW_LATENCY
insieme a un callback ad alta priorità. Segui questo esempio:
// 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 non è completamente thread-safe. Non puoi chiamare alcune delle funzioni AAudio contemporaneamente da più thread alla volta. Questo perché AAudio evita di utilizzare i mutex, che possono causare preemption dei thread e glitch.
Per sicurezza, non chiamare AAudioStream_waitForStateChange()
né leggere o scrivere nello stesso stream da due thread diversi. Analogamente, non chiudere uno stream in un thread mentre lo leggi o ci scrivi in un altro thread.
Le chiamate che restituiscono le impostazioni dello stream, come AAudioStream_getSampleRate()
e AAudioStream_getChannelCount()
, sono sicure per i thread.
Queste chiamate sono anche sicure per i thread:
AAudio_convert*ToText()
AAudio_createStreamBuilder()
AAudioStream_get*()
ad eccezione diAAudioStream_getTimestamp()
Problemi noti
- La latenza audio è elevata per la scrittura bloccante() perché la release DP2 di Android O non utilizza un canale FAST. Utilizza un callback per ottenere una latenza inferiore.
Risorse aggiuntive
Per scoprire di più, utilizza le seguenti risorse: