Audio

AAudio è una nuova API per Android C 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 nei flussi.

L'API AAudio è minima per definizione, non esegue queste funzioni:

  • Enumerazione del dispositivo audio
  • Routing automatico tra endpoint audio
  • File I/O
  • Decodifica dell'audio compresso
  • Presentazione automatica di tutti gli input/stream in un singolo callback.

Come 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 la tua app e gli input e gli output audio sul tuo dispositivo Android. L'app trasmette i dati in entrata e in uscita leggendo e scrivendo in stream audio, rappresentati dalla struttura AAudioStream. Le chiamate in lettura/scrittura possono essere bloccate o non bloccabili.

Un flusso è definito da:

  • Il dispositivo audio che è l'origine o il sink per i 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 sorgente o sink per un flusso continuo di dati audio digitali. Non confondere un dispositivo audio (un microfono o un auricolare integrato) con il dispositivo Android (il telefono o l'orologio) su cui è installata 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 ogni 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 consentire ad AAudio di scegliere il dispositivo principale predefinito anziché specificarne uno autonomamente.

Il dispositivo audio collegato a uno stream determina se lo stream è per input o output. Un flusso può spostare dati solo in una direzione. Quando definisci uno stream ne imposti anche la direzione. Quando apri uno stream, Android controlla che il dispositivo audio e la direzione dello stream siano d'accordo.

Modalità di condivisione

Uno stream dispone di una modalità di condivisione:

  • AAUDIO_SHARING_MODE_EXCLUSIVE indica che lo stream ha accesso esclusivo al proprio 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. È probabile che gli stream esclusivi abbiano una latenza minore, ma è anche più probabile che vengano disconnessi. Devi chiudere gli stream esclusivi non appena non ti servono più, in modo che altre app possano accedere al dispositivo. Gli stream esclusivi forniscono la latenza più bassa possibile.
  • AAUDIO_SHARING_MODE_SHARED consente ad AAudio di combinare audio. AAudio mescola 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 attraverso un flusso hanno i consueti attributi audio digitali. Ecco come fare:

  • Formato dati di esempio
  • Conteggio canali (esempi per frame)
  • Frequenza di campionamento

AAudio consente i seguenti formati di esempio:

formato_audio_t Tipo di dati C Note
FORMATO_APC_I_16 int16_t campioni comuni a 16 bit, formato Q0.15
FLOAT_FORMAT_PCM_AUDIO mobile Da -1,0 a +1,0
FORMATO_PRODOTTI_PC_I24_AUDIO uint8_t in gruppi di 3 pacchetti di campioni a 24 bit, formato Q0.23
FORMATO_PC_I_3AAUDIO int32_t campioni comuni a 32 bit, formato Q0.31
AAUDIO_FORMAT_IEC61937 uint8_t Audio compresso con wrapping in IEC61937 per passthrough HDMI o S/PDIF

Se richiedi un formato di esempio specifico, lo stream lo utilizzerà, anche se quest'ultimo non è ottimale per il dispositivo. Se non specifichi un formato di esempio, AAudio ne sceglierà uno ottimale. Dopo aver aperto il flusso, 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.

  1. Crea un AAudioStreamBuilder:

    AAudioStreamBuilder *builder;
    aaudio_result_t result = AAudio_createStreamBuilder(&builder);
    

  2. Imposta la configurazione dello stream audio nel builder, utilizzando le funzioni del builder che corrispondono ai parametri dello stream. Sono disponibili le seguenti funzioni di insieme opzionali:

    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 l'ID dispositivo, 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 il sistema assegni il valore ottimale non specificando alcun parametro o non impostandolo su AAUDIO_UNSPECIFIED.

    Per sicurezza, controlla lo stato dello stream audio dopo averlo creato, come spiegato nel passaggio 4 riportato di seguito.

  3. Quando è configurato AAudioStreamBuilder, utilizzalo per creare un flusso:

    AAudioStream *stream;
    result = AAudioStreamBuilder_openStream(builder, &stream);
    

  4. Dopo aver creato il flusso, verifica la configurazione. Se hai specificato il formato di campionamento, la frequenza di campionamento o gli esempi per frame, questi non cambieranno. Se hai specificato la modalità di condivisione o la capacità del buffer, questa potrebbe cambiare a seconda delle funzionalità del dispositivo audio dello stream e del dispositivo Android su cui è in esecuzione. Per una buona programmazione difensiva, devi controllare la configurazione del flusso prima di utilizzarlo. Sono disponibili funzioni per recuperare l'impostazione del flusso che corrisponde a ogni impostazione del builder:

    AAudioStreamBuilder_setDeviceId() AAudioStream_getDeviceId()
    AAudioStreamBuilder_setDirection() AAudioStream_getDirection()
    AAudioStreamBuilder_setSharingMode() AAudioStream_getSharingMode()
    AAudioStreamBuilder_setSampleRate() AAudioStream_getSampleRate()
    AAudioStreamBuilder_setChannelCount() AAudioStream_getChannelCount()
    AAudioStreamBuilder_setFormat() AAudioStream_getFormat()
    AAudioStreamBuilder_setBufferCapacityInFrames() AAudioStream_getBufferCapacityInFrames()

  5. Puoi salvare il builder e riutilizzarlo in futuro per creare più flussi. Tuttavia, se non prevedi di utilizzarlo più, devi eliminarlo.

    AAudioStreamBuilder_delete(builder);
    

Utilizzo di uno stream audio

Transizioni di stato

Solitamente, 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
  • Interrotto

I dati passano attraverso uno stream solo quando è in stato Iniziato. Per spostare un flusso 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 solo la pausa o lo svuotamento in base a un flusso di output:

Queste funzioni sono asincrone e la modifica dello stato non avviene immediatamente. Quando richiedi una modifica dello stato, il flusso sposta uno degli stati temporanei corrispondenti:

  • Avvio in corso
  • In pausa
  • Faccina con guance rosse
  • In sosta
  • Chiusura

Il diagramma di stato seguente mostra gli stati stabili come rettangoli arrotondati e gli stati temporanei come rettangoli tratteggiati. Anche se non viene mostrato, puoi chiamare close() da qualsiasi stato

Ciclo di vita AAudio

AAudio non fornisce callback per avvisarti di modifiche di stato. Una funzione speciale, AAudioStream_waitForStateChange(stream, inputState, nextState, timeout), può essere utilizzata per attendere un cambiamento di stato.

La funzione non rileva automaticamente un cambiamento di stato e non attende uno stato specifico. Attendi che lo stato attuale sia diverso da inputState, che specifichi.

Ad esempio, dopo una richiesta di pausa, uno stream dovrebbe passare immediatamente allo stato temporaneo In pausa e arrivare in un secondo momento allo stato In pausa, anche se non esiste alcuna garanzia. Poiché non puoi aspettare lo stato In pausa, utilizza waitForStateChange() per attendere uno stato diverso da quello in 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 del flusso non è In pausa (il inputState, che abbiamo ipotizzato fosse lo stato attuale al momento della chiamata), la funzione torna immediatamente. In caso contrario, si blocca fino a quando lo stato non viene più messo in pausa o il timeout scade. Quando viene restituita la funzione, il parametro nextState mostra lo stato attuale del flusso.

Puoi utilizzare questa stessa tecnica dopo aver chiamato l'inizio, l'arresto o l'eliminazione della richiesta, utilizzando lo stato temporaneo corrispondente come inputState. Non chiamare waitForStateChange() dopo aver chiamato AAudioStream_close(), perché lo stream verrà eliminato alla chiusura. 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:

Per una lettura o scrittura di blocco che trasferisce il numero di frame specificato, imposta timeoutNanos maggiore di zero. Per una chiamata che non comporta alcun blocco, 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 problema audio. Puoi riempire il buffer con zeri per creare un abbandonamento 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 creare un buffer dello stream prima di avviarlo scrivendo dati o silenziando l'audio. 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().

Chiudere uno stream audio

Quando hai finito di utilizzare uno stream, chiudilo:

AAudioStream_close(stream);

Dopo aver chiuso uno stream, non puoi utilizzarlo con alcuna funzione basata su stream AAudio.

Stream audio disconnesso

Uno stream audio può disconnettersi in qualsiasi momento se si verifica uno dei seguenti eventi:

  • Il dispositivo audio associato non è più connesso (ad esempio quando le cuffie sono scollegate).
  • Si è verificato un errore internamente.
  • Un dispositivo audio non è più il dispositivo audio principale.

Quando un flusso è disconnesso, ha lo stato "Disconnettito" 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 diretta), non riceverai alcun codice di ritorno quando il flusso è disconnesso. Per ricevere notifiche quando succede, 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 effettuate da un altro thread. In caso contrario, potresti avere un deadlock.

Tieni presente che se apri un nuovo stream, questo potrebbe avere impostazioni diverse da quello originale (ad esempio framePerBurst):

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 del rendimento

Puoi ottimizzare le prestazioni di un'applicazione audio regolandone i buffer interni e utilizzando speciali thread ad alta priorità.

Ottimizzare i buffer per ridurre al minimo la latenza

AAudio trasmette i dati dentro e fuori dai buffer interni che mantiene, uno per ogni dispositivo audio.

La capacità del buffer è la quantità totale di dati che un buffer può contenere. Puoi chiamare AAudioStreamBuilder_setBufferCapacityInFrames() per impostare la capacità. Il metodo limita la capacità che puoi assegnare al valore massimo consentito dal dispositivo. Utilizza AAudioStream_getBufferCapacityInFrames() per verificare la capacità effettiva del buffer.

Un'app non deve utilizzare tutta la capacità di un buffer. Un audio riempie il buffer fino a una dimensione che puoi impostare. Le dimensioni di un buffer possono non essere superiori alla sua capacità e spesso sono più piccole. Il controllo della dimensione del buffer determina il numero di burst necessari per riempirlo e, quindi, controllare la latenza. Utilizza i metodi AAudioStreamBuilder_setBufferSizeInFrames() e AAudioStreamBuilder_getBufferSizeInFrames() per lavorare con 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 e di solito è più piccolo delle dimensioni del buffer letto. Il sistema controlla le dimensioni e la frequenza di burst, in genere queste proprietà sono dettate dai circuiti dei dispositivi audio. Anche se non puoi modificare le dimensioni di un burst o del tasso di burst, puoi impostare la dimensione del buffer interno in base al numero di burst che contiene. In genere, ottieni la latenza più bassa se la dimensione del buffer di AAudioStream è un multiplo delle dimensioni del burst registrate.

      Buffering audio

Un modo per ottimizzare la dimensione del buffer consiste nell'iniziare con un buffer grande e poi abbassarlo gradualmente fino a quando non inizia un ciclo inferiore, quindi spingerlo nuovamente in alto. In alternativa, puoi iniziare con una dimensione del buffer ridotta e, se questo produce insufficiente, aumentare la dimensione del buffer finché l'output non torna bene.

Questo processo può avvenire molto rapidamente, possibilmente prima che l'utente riproduca il primo suono. Ti consigliamo di eseguire per prima cosa il buffer iniziale, utilizzando la modalità silenziosa, in modo che l'utente non senta alcun problema audio. Le prestazioni del sistema potrebbero variare nel tempo (ad esempio, l'utente potrebbe disattivare la modalità aereo). Poiché l'ottimizzazione del buffer aggiunge un carico di lavoro minimo, l'app può farlo in modo continuo mentre legge o scrive dati in uno stream.

Ecco un esempio di loop 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 un flusso di input. I flussi di input vengono eseguiti il più velocemente possibile, cercando di mantenere al minimo la quantità di dati presenti nel buffer, quindi di riempire l'app quando viene prerilasciata.

Utilizzo di un callback a priorità elevata

Se la tua app legge o scrive dati audio da un normale thread, potrebbe essere prerilasciata o potrebbe verificarsi un tremolio. Questo può causare problemi di audio. L'utilizzo di buffer più grandi può proteggere da questi problemi, ma un buffer grande presenta anche una latenza audio più lunga. Per le applicazioni che richiedono bassa latenza, uno stream audio può utilizzare una funzione di callback asincrona per trasferire i dati da e verso la tua app. AAudio esegue il callback in un thread con priorità più elevata con 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);

Usa l'edificio dello stream per registrare il callback:

AAudioStreamBuilder_setDataCallback(builder, myCallback, myUserData);

Nel caso più semplice, il flusso esegue periodicamente la funzione di callback per acquisire i dati per la burst successiva.

La funzione di callback non deve eseguire una lettura o una scrittura sul flusso che l'ha richiamato. 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'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 un unico stream come master e passare i puntatori ad altri flussi nei dati utente. Registra un callback per lo stream principale. Utilizza quindi I/O non bloccanti sugli altri flussi. Di seguito è riportato un esempio di callback di andata e ritorno che trasmette uno stream di input a uno stream di output. Lo stream delle chiamate principale è lo stream di output. Lo stream di input è incluso nei dati utente.

Il callback esegue una lettura senza bloccare dallo stream di input che posiziona 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 i flussi 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à di rendimento

Ogni AAudioStream dispone di una modalità Performance che ha un grande effetto sul comportamento della tua app. Esistono tre modalità:

  • AAUDIO_PERFORMANCE_MODE_NONE è la modalità predefinita. Utilizza uno stream di base che bilancia la latenza e il risparmio energetico.
  • AAUDIO_PERFORMANCE_MODE_LOW_LATENCY utilizza buffer più piccoli e un percorso dati ottimizzato per una latenza ridotta.
  • AAUDIO_PERFORMANCE_MODE_POWER_SAVING utilizza buffer interni più grandi e un percorso dati che compromette la latenza per una potenza inferiore.

Puoi selezionare la modalità rendimento chiamando setPerformanceMode() e scoprire la modalità attuale chiamando getPerformanceMode().

Se la bassa latenza è più importante del risparmio energetico nella tua applicazione, usa AAUDIO_PERFORMANCE_MODE_LOW_LATENCY. È utile per le app molto interattive, come i giochi o i sintetizzatori da tastiera.

Se il risparmio energetico è più importante della bassa latenza nell'applicazione, utilizza AAUDIO_PERFORMANCE_MODE_POWER_SAVING. Questa opzione è tipica per le app che riproducono musica generata in precedenza, come i file player MIDI o l'audio in streaming.

Nella versione attuale di AAudio, per ottenere la minore latenza possibile, devi utilizzare la modalità prestazioni AAUDIO_PERFORMANCE_MODE_LOW_LATENCY e un callback ad alta priorità. Segui l'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);

Sicurezza filettatura

L'API AAudio non è completamente thread-safe. Non puoi chiamare contemporaneamente alcune funzioni AAudio da più di un thread alla volta. Questo perché AAudio evita di utilizzare mutex, che possono causare prerilasci e problemi di thread.

Per sicurezza, non chiamare AAudioStream_waitForStateChange() e non leggere o scrivere nello stesso stream da due thread diversi. Analogamente, non chiudere uno stream in un thread mentre leggi o scrivi in un altro thread.

Le chiamate che restituiscono le impostazioni dello stream, come AAudioStream_getSampleRate() e AAudioStream_getChannelCount(), sono sicure per il thread.

Queste chiamate sono anche sicure per i thread:

  • AAudio_convert*ToText()
  • AAudio_createStreamBuilder()
  • AAudioStream_get*() ad eccezione di AAudioStream_getTimestamp()

Problemi noti

  • La latenza audio è alta per bloccare write() perché la release Android O DP2 non utilizza una traccia VELOCE. Utilizza un callback per ridurre la latenza.

Risorse aggiuntive

Per saperne di più, consulta le risorse seguenti:

API Reference

Codelab

Video