Audio

AAudio è una nuova API Android C introdotta con la release di 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 ha una progettazione minima e non svolge le seguenti funzioni:

  • Enumerazione del dispositivo audio
  • Routing automatico tra endpoint audio
  • I/O file
  • Decodifica dell'audio compresso
  • Presentazione automatica di tutti gli input/stream in un'unica 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

Il servizio AAudio sposta i dati audio tra la tua app e gli ingressi e le uscite audio del tuo dispositivo Android. L'app trasmette i dati in entrata e in uscita leggendo e scrivendo agli stream audio, rappresentati dalla struttura AAudioStream. Le chiamate di lettura/scrittura possono bloccare o non bloccare.

Uno stream viene definito come segue:

  • Il dispositivo audio che funge da origine o 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 (microfono integrato o cuffie Bluetooth) con il dispositivo Android (il telefono o l'orologio) su cui è installata l'app.

Puoi usare il metodo AudioManagergetDevices() per trovare i dispositivi audio disponibili sul tuo dispositivo Android. Il metodo restituisce informazioni relative ai 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 consentire ad AAudio di scegliere il dispositivo principale predefinito anziché specificarne uno manualmente.

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

Modalità di condivisione

Uno stream ha 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 altri stream audio. Se il dispositivo audio è già in uso, lo stream potrebbe non avere l'accesso esclusivo. È probabile che gli stream esclusivi abbiano una latenza più bassa, ma che si disconnettano. Dovresti chiudere gli stream esclusivi 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 audio. Un'impostazione Audio unisce tutti gli stream condivisi assegnati allo stesso dispositivo.

Puoi impostare esplicitamente la modalità di condivisione quando crei uno stream. Per impostazione predefinita, la modalità di condivisione è SHARED.

Formato audio

I dati trasmessi attraverso uno stream hanno gli attributi audio digitali standard. Di seguito sono riportate le informazioni che seguono.

  • Formato dei dati di esempio
  • Numero di canali (campioni per frame)
  • Frequenza di campionamento

AAudio consente i seguenti formati di esempio:

formato_audio Tipo di dati C Notes
FORMATO_AUDIO_PCM_I16 int16_t campioni comuni a 16 bit, formato Q0.15
AAUDIO_FORMAT_PCM_FLOAT numero in virgola mobile Da -1,0 a +1,0
FORMATO_AUDIO_PCM_I24_PACKED uint8_t in gruppi di 3 campioni compressi a 24 bit, formato Q0.23
FORMATO_AUDIO_PCM_I32 int32_t campioni comuni a 32 bit, formato Q0.31
FORMATO_AUDIO_IEC61937 Uint8_t audio compresso racchiuso in IEC61937 per il passthrough HDMI o S/PDIF

Se richiedi un formato di esempio specifico, lo stream utilizzerà quel formato, anche se il formato non è ottimale per il dispositivo. Se non specifichi un formato di esempio, AAudio ne sceglierà uno ottimale. Dopo l'apertura dello stream, devi eseguire una query sul formato dei dati di esempio e, se necessario, convertire i dati, 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 raccolta 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 generatore, utilizzando le funzioni del generatore che corrispondono ai parametri dello stream. Sono disponibili queste funzioni di insieme 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, come 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 del flusso, 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 evitando di specificare il parametro o impostandolo su AAUDIO_UNSPECIFIED.

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

  3. Una volta configurato AAudioStreamBuilder, utilizzalo per creare uno stream:

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

  4. Dopo aver creato il flusso, verificane la configurazione. Se hai specificato formato di esempio, frequenza di campionamento o campioni per frame, questi elementi non subiranno modifiche. Se hai specificato la modalità di condivisione o la capacità di buffer, potrebbero cambiare in base alle funzionalità del dispositivo audio dello stream e del dispositivo Android su cui è in esecuzione. Per una buona programmazione difensiva, dovresti controllare la configurazione del flusso prima di utilizzarlo. Esistono 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ù stream. Tuttavia, se non prevedi di utilizzarlo più, dovresti eliminarlo.

    AAudioStreamBuilder_delete(builder);
    

Utilizzare 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 con occhi spalancati
  • Interrotto

I dati passano attraverso un flusso solo quando è nello stato Iniziato. Per spostare un flusso da uno stato all'altro, 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 solo in uno stream di output:

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

  • Avvio in corso
  • In pausa
  • Arrossire
  • In sosta
  • Chiusura

Il diagramma degli stati riportato di seguito mostra gli stati stabili come rettangoli arrotondati e gli stati temporanei come rettangoli punteggiati. Anche se non è visualizzato, puoi chiamare close() da qualsiasi stato

Ciclo di vita AAudio

AAudio non fornisce callback per avvisarti delle modifiche dello 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. Attende che lo stato attuale sia diverso da inputState, che hai specificato.

Ad esempio, dopo aver richiesto di mettere in pausa, un flusso deve entrare immediatamente nello stato temporaneo In pausa e arrivare in un secondo momento allo stato In pausa, anche se non vi è alcuna garanzia che lo farà. Poiché non puoi attendere lo stato In pausa, utilizza waitForStateChange() per attendere qualsiasi stato diverso da In pausa. A tale scopo, procedi nel seguente modo:

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 (inputState, che presumevamo fosse lo stato attuale al momento della chiamata), la funzione restituisce immediatamente. In caso contrario, si blocca finché lo stato non è più In pausa o fino alla scadenza del timeout. Quando la funzione restituisce, il parametro nextState mostra lo stato attuale del flusso.

Puoi utilizzare questa stessa tecnica dopo aver chiamato l'avvio, l'arresto o lo svuotamento della richiesta, utilizzando lo stato temporaneo corrispondente come inputState. Non chiamare waitForStateChange() dopo aver chiamato AAudioStream_close(), poiché lo stream verrà eliminato alla chiusura. Non chiamare AAudioStream_close() mentre waitForStateChange() è in esecuzione in un altro thread.

Lettura e scrittura di 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 specificato di frame, imposta timeoutNanos maggiore di zero. Per una chiamata che non blocca, 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 abbandono 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 preparare il buffer del flusso prima di avviarlo scrivendo dati o silenziando al suo interno. 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 di AAudio.

Stream audio disconnesso

Uno stream audio può interrompersi 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 interno.
  • Un dispositivo audio non è più il dispositivo audio principale.

Quando uno stream è disconnesso, 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 restituito quando lo stream viene disconnesso. Per informarti quando ciò accade, 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 avere un deadlock.

Tieni presente che se apri un nuovo stream, questo potrebbe avere impostazioni diverse rispetto allo stream 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 regolando i buffer interni e utilizzando speciali thread ad alta priorità.

Ottimizzazione del buffer per ridurre al minimo la latenza

Un dispositivo audio trasmette i dati in entrata e in uscita dai buffer interni che gestisce, 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 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. Un audio riempie un buffer fino a una dimensione che puoi impostare. La dimensione di un buffer non può essere superiore alla sua capacità e spesso è inferiore. Controllando le dimensioni del buffer, determini il numero di burst necessari per riempirlo e quindi controlli la latenza. Utilizza i metodi AAudioStreamBuilder_setBufferSizeInFrames() e AAudioStreamBuilder_getBufferSizeInFrames() per lavorare con la dimensione del buffer.

Quando un'applicazione riproduce l'audio, scrive in un buffer e blocca l'audio fino al completamento della scrittura. Il componente AAudio legge dal buffer a raffiche discrete. Ogni burst contiene un numero multiplo di frame audio e di solito è inferiore alle dimensioni del buffer letto. Il sistema controlla le dimensioni e la frequenza delle raffiche. Queste proprietà sono solitamente dettate dai circuiti del dispositivo audio. Sebbene non sia possibile modificare le dimensioni di una raffica o la velocità di burst, puoi impostare le dimensioni 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 riportate.

      Buffering AAudio

Un modo per ottimizzare la dimensione del buffer consiste nell'iniziare con un buffer grande e ridurlo gradualmente fino all'inizio degli sottoscatti, quindi spostare nuovamente il buffer verso l'alto. In alternativa, puoi iniziare con una dimensione del buffer ridotta e, se questo produce valori inferiori, aumenta la dimensione del buffer fino a quando l'output scorre di nuovo in modo pulito.

Questa procedura può essere eseguita molto rapidamente, possibilmente prima che l'utente riproduca il primo suono. Ti consigliamo di eseguire prima il dimensionamento iniziale del buffer, usando la modalità silenziosa, in modo che l'utente non senta problemi di audio. Le prestazioni del sistema possono cambiare nel tempo (ad esempio, l'utente potrebbe disattivare la modalità aereo). Poiché l'ottimizzazione del buffer aggiunge un overhead molto ridotto, l'app può farlo continuamente mentre legge o scrive dati in un flusso.

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 vi è alcun vantaggio nell'utilizzare questa tecnica per ottimizzare la dimensione 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 nel buffer, per poi riempire quando l'app viene prerilasciata.

Utilizzare un callback ad alta priorità

Se l'app legge o scrive dati audio da un thread ordinario, questa potrebbe essere prerilasciata o potrebbe verificarsi un tremolio del tempo. Potrebbero verificarsi problemi audio. L'utilizzo di buffer più grandi può proteggerti da questi glitch, ma un buffer più grande 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 i dati da e verso la tua app. AAudio esegue il callback in un thread con priorità più alta che ha 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 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 il burst successivo.

La funzione callback non deve eseguire operazioni di lettura o scrittura sullo stream che l'ha richiamata. Se il callback appartiene a uno stream di input, il codice dovrebbe 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 onda sinusoidale come questo:

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 flusso come master e passare i puntatori ad altri flussi nei dati utente. Registra un callback per lo stream principale. Quindi, utilizza l'I/O che non blocca sugli altri stream. Ecco un esempio di callback round-trip che passa uno stream di input a uno stream di output. Il flusso della chiamata principale è il flusso di output. Il flusso di input è incluso nei dati utente.

Il callback esegue una lettura non bloccante dal flusso di input che inserisce 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 potrebbe non corrispondere, a condizione che il codice gestisca le traduzioni correttamente.

Impostazione della modalità Prestazioni

Ogni AAudioStream include una modalità prestazioni 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 ridurre la latenza.
  • AAUDIO_PERFORMANCE_MODE_POWER_SAVING utilizza buffer interni più grandi e un percorso dati che scambia la latenza con una potenza inferiore.

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

Se la bassa latenza è più importante del risparmio energetico nella tua applicazione, utilizza 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. Si tratta di un comportamento tipico delle app che riproducono musica generata in precedenza, come lo streaming audio o i lettori di file MIDI.

Nella versione attuale di AAudio, per ottenere la latenza più bassa possibile, devi usare la modalità performance 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);

Sicurezza dei thread

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

Per sicurezza, non chiamare AAudioStream_waitForStateChange() né eseguire operazioni di lettura o scrittura sullo 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 impostazioni dello stream, come AAudioStream_getSampleRate() e AAudioStream_getChannelCount(), sono sicure in base ai thread.

Anche queste chiamate sono compatibili con i thread:

  • AAudio_convert*ToText()
  • AAudio_createStreamBuilder()
  • AAudioStream_get*() tranne AAudioStream_getTimestamp()

Problemi noti

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

Risorse aggiuntive

Per saperne di più, consulta le seguenti risorse:

API Reference

Codelab

Video