AAudio

A AAudio é uma nova API em C do Android introduzida no Android O. Ela foi projetada para aplicativos de áudio de alto desempenho que precisam de baixa latência. Para se comunicar com a AAudio, os apps leem e gravam dados nos streams.

A API AAudio foi criada de maneira minimalista e não tem as seguintes funções:

  • Enumeração do dispositivo de áudio
  • Roteamento automático entre endpoints de áudio
  • E/S de arquivos
  • Decodificação de áudio compactado
  • Apresentação automática de todas as entradas/streams em um único callback

Primeiros passos

Você pode chamar a AAudio usando o código C++. Para adicionar o conjunto de recursos da AAudio ao seu app, inclua o arquivo principal AAudio.h:

#include <aaudio/AAudio.h>

Streams de áudio

A AAudio transfere dados de áudio entre seu aplicativo e as entradas e saídas de áudio no dispositivo Android. Seu aplicativo transmite e recebe dados lendo e gravando streams de áudio, representados pela estrutura AAudioStream. As chamadas de leitura/gravação podem ser bloqueadoras ou não bloqueadoras.

Um stream é definido pelos seguintes elementos:

  • O dispositivo de áudio que é a origem ou o coletor dos dados no stream.
  • O modo de compartilhamento que determina se um stream tem acesso exclusivo a um dispositivo de áudio que poderia ser compartilhado entre vários streams.
  • O formato dos dados de áudio no stream.

Dispositivo de áudio

Cada stream é associado a um único dispositivo de áudio.

Um dispositivo de áudio é uma interface de hardware ou um endpoint virtual que atua como origem ou coletor de um stream contínuo de dados de áudio digital. Não confunda o dispositivo de áudio (um microfone integrado ou fone de ouvido Bluetooth) com o dispositivo Android (o smartphone ou o relógio) que está executando o app.

Use o método AudioManager do getDevices() para descobrir os dispositivos de áudio disponíveis no dispositivo Android. Esse método retorna informações sobre o type de cada dispositivo.

Cada dispositivo de áudio tem um ID exclusivo no dispositivo Android. É possível usar esse código para vincular um stream a um dispositivo de áudio específico. No entanto, na maioria dos casos, você pode deixar a AAudio escolher o dispositivo principal padrão em vez de especificá-lo manualmente.

O dispositivo de áudio associado a um stream determina se ele é usado para entrada ou saída. Um stream só pode mover dados em uma direção. Ao configurar um stream, é necessário definir também a direção dele. Ao abrir um stream, o Android verifica se o dispositivo de áudio e a direção do stream estão de acordo.

Modo de compartilhamento

Um stream tem um modo de compartilhamento:

  • AAUDIO_SHARING_MODE_EXCLUSIVE significa que o stream tem acesso exclusivo ao dispositivo de áudio. O dispositivo não pode ser usado por outro stream de áudio. Se o dispositivo de áudio já estiver em uso, talvez o stream não consiga ter acesso exclusivo. Os streams exclusivos normalmente têm menor latência, mas são mais propensos a desconexões. É preciso fechar os streams exclusivos quando eles não forem mais necessários, de modo que outros aplicativos possam acessar o dispositivo. Streams exclusivos oferecem a menor latência possível.
  • AAUDIO_SHARING_MODE_SHARED permite que a AAudio combine áudios. A AAudio combina todos os streams compartilhados que foram atribuídos ao mesmo dispositivo.

Você pode definir o modo de compartilhamento explicitamente ao criar um stream. Por padrão, o modo de compartilhamento é SHARED.

Formato de áudio

Os dados transmitidos por um stream têm os atributos comuns de áudio digital. Esses atributos são os seguintes:

  • Formato de dados de amostra
  • Contagem de canais (amostras por frame)
  • Taxa de amostragem

A AAudio aceita os seguintes formatos de amostra:

aaudio_format_t tipo de dados C Notas
AAUDIO_FORMAT_PCM_I16 int16_t amostras comuns de 16 bits, formato Q0.15
AAUDIO_FORMAT_PCM_FLOAT ponto flutuante -1,0 a +1,0
AAUDIO_FORMAT_PCM_I24_PACKED uint8_t em grupos de 3 amostras empacotadas de 24 bits, formato Q0.23
AAUDIO_FORMAT_PCM_I32 int32_t amostras comuns de 32 bits, formato Q0.31
AAUDIO_FORMAT_IEC61937 uint8_t áudio compactado unido em IEC61937 para passagem HDMI ou S/PDIF

Se você solicitar um formato de amostra específico, o stream vai usar esse formato, mesmo se ele não for o ideal para o dispositivo. Se você não especificar um formato de amostra, o AAudio escolherá um formato ideal. Após a abertura do stream, você precisará consultar o formato de dados de amostra e converter os dados, se necessário, como neste exemplo:

aaudio_format_t dataFormat = AAudioStream_getDataFormat(stream);
//... later
if (dataFormat == AAUDIO_FORMAT_PCM_I16) {
     convertFloatToPcm16(...)
}

Criar um stream de áudio

A biblioteca AAudio segue um padrão de design de construtor e fornece o AAudioStreamBuilder.

  1. Crie um AAudioStreamBuilder:

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

  2. Defina a configuração do stream de áudio no construtor, usando as funções correspondentes aos parâmetros do stream. As seguintes funções de conjunto opcionais estão disponíveis:

    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);
    

    Esses métodos não comunicam erros, como constantes indefinidas ou valores fora do intervalo.

    Se deviceId não for especificado, o padrão será o dispositivo de saída principal. Se a direção do stream não for especificada, o padrão será um stream de saída. É possível definir explicitamente um valor ou deixar que o sistema atribua o valor ideal para todos os outros parâmetros, basta não especificá-los ou defini-los como AAUDIO_UNSPECIFIED.

    Por segurança, verifique o estado do stream de áudio depois de criá-lo, conforme indicado na etapa 4 abaixo.

  3. Quando o AAudioStreamBuilder estiver configurado, use-o para criar um stream:

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

  4. Depois de criar o stream, verifique a configuração dele. Se você tiver especificado o formato de amostragem, a taxa de amostragem ou a quantidade de amostras por frame, esses valores não serão alterados. Se você tiver especificado o modo de compartilhamento ou a capacidade do buffer, esses valores poderão mudar dependendo dos recursos do dispositivo de áudio do stream e do dispositivo Android em que ele está sendo executado. Para programar de maneira segura, é necessário verificar a configuração do stream antes de usá-lo. Existem funções para recuperar a configuração de stream que corresponde a cada configuração do construtor:

    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. Você pode salvar o construtor e reutilizá-lo no futuro para criar mais streams. No entanto, caso não planeje mais usá-lo, é importante excluí-lo.

    AAudioStreamBuilder_delete(builder);
    

Usar um stream de áudio

Transições de estado

Quando não estão com o estado de erro “Desconectado”, descrito no final desta seção, os streams da AAudio geralmente estão em um dos cinco estados estáveis:

  • Aberto
  • Iniciado
  • Pausado
  • Limpo
  • Parado

Os dados fluem pelo stream apenas quando ele está no estado “iniciado”. Para mudar os estados de um stream, use as funções que solicitam a transição de estado:

aaudio_result_t result;
result = AAudioStream_requestStart(stream);
result = AAudioStream_requestStop(stream);
result = AAudioStream_requestPause(stream);
result = AAudioStream_requestFlush(stream);

Só é possível solicitar a pausa ou limpeza em streams de saída:

Essas funções são assíncronas, e a mudança de estado não acontece imediatamente. Ao solicitar uma alteração de estado, o stream passa para um dos seguintes estados transitórios correspondentes:

  • Iniciando
  • Pausando
  • Limpando
  • Parando
  • Fechando

O diagrama abaixo mostra os estados estáveis como retângulos arredondados e os transitórios como retângulos pontilhados. Embora não seja exibida, é possível chamar a função close() em qualquer estado

Ciclo de vida da AAudio

A AAudio não oferece callbacks para alertar sobre mudanças de estado. A função especial AAudioStream_waitForStateChange(stream, inputState, nextState, timeout) pode ser usada para aguardar uma mudança de estado.

A função não detecta mudanças de estado e não aguarda um estado específico. Ela aguarda até que o estado atual não seja o inputState especificado.

Por exemplo, depois de solicitar a pausa, o stream entrará imediatamente no estado temporário “Pausando” e, algum tempo depois, passará para o estado “Pausado”. No entanto, não há garantia de que isso ocorrerá. Como não é possível esperar pelo estado “Pausado”, use waitForStateChange() para aguardar qualquer estado que não seja “Pausando”. Veja como isso é feito:

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 o estado do stream não for “Pausando” (o inputState, que assumimos ser o estado atual no momento da chamada), a função retornará imediatamente. Caso contrário, ela ficará bloqueada até que o estado não seja mais “Pausando” ou o tempo limite expire. Quando a função retornar, o parâmetro nextState mostrará o estado atual do stream.

Use essa mesma técnica após chamar a solicitação para iniciar, parar ou limpar, usando o estado transitório correspondente como inputState. Não chame waitForStateChange() após chamar AAudioStream_close(), porque o stream será excluído assim que ele for fechado. Não chame AAudioStream_close() enquanto waitForStateChange() estiver sendo executado em outra linha de execução.

Ler e gravar um stream de áudio

Há duas maneiras de processar os dados em um stream após ele ser iniciado:

Para uma leitura ou gravação de método de bloqueio que transfira o número especificado de frames, defina timeoutNanos como um número maior que zero. Para chamadas que não fazem bloqueio, configure timeoutNanos como zero. Nesse caso, o resultado será o número de frames transferidos.

Ao ler a entrada, é necessário verificar se o número correto de frames foi lido. Caso esse número não esteja correto, talvez o buffer tenha dados desconhecidos que podem causar falhas no áudio. É possível preencher o buffer com zeros para criar dropouts silenciosos:

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);
}

É possível preparar o buffer do stream antes de iniciá-lo, gravando ou silenciando dados. Isso precisa ser feito em uma chamada que não bloqueie, com timeoutNanos definido como zero.

Os dados no buffer precisam corresponder ao formato de dados retornado por AAudioStream_getDataFormat().

Fechar um stream de áudio

Feche o stream quando terminar de usá-lo:

AAudioStream_close(stream);

Depois de fechar um stream, não será possível usá-lo com funções baseadas em streams da AAudio.

Desconectar streams de áudio

Um stream de áudio poderá ser desconectado a qualquer momento se:

  • o dispositivo de áudio associado não estiver mais conectado (por exemplo, se os fones de ouvido forem desconectados);
  • ocorrer um erro interno;
  • o dispositivo de áudio não for mais o principal.

Quando um stream é desconectado, ele recebe o estado "Desconectado" e todas as tentativas de executar AAudioStream_write() ou outras funções retornam um erro. É sempre necessário parar e fechar um stream desconectado, independente do código de erro.

Se estiver usando um callback de dados, em vez de um dos métodos diretos de leitura/gravação, você não receberá nenhum código de retorno quando o stream for desconectado. Para ser informado quando isso acontecer, escreva uma função AAudioStream_errorCallback e a registre usando AAudioStreamBuilder_setErrorCallback().

Se você receber uma notificação sobre a desconexão de uma linha de execução de callback de erro, a parada e o fechamento do stream precisarão ser feitos em outra linha. Caso contrário, você poderá ter um impasse.

Caso um novo stream seja aberto, talvez ele tenha configurações diferentes do original (por exemplo, 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.
}

Otimização do desempenho

Para otimizar o desempenho de um aplicativo de áudio, ajuste os buffers internos e use linhas de execução especiais de alta prioridade.

Ajustar buffers para minimizar a latência

A AAudio transmite dados para dentro e para fora dos buffers internos que ela mantém, um para cada dispositivo de áudio.

A capacidade do buffer é a quantidade total de dados que ele é capaz de conter. Você pode ligar AAudioStreamBuilder_setBufferCapacityInFrames() para definir a capacidade. Esse método limita a capacidade que você pode alocar ao valor máximo permitido pelo dispositivo. Use AAudioStream_getBufferCapacityInFrames() para verificar a capacidade real do buffer.

O aplicativo não precisa usar toda a capacidade do buffer. A AAudio preenche o buffer até o tamanho definido. O tamanho de um buffer não pode ser maior que a capacidade dele. Normalmente, ele é menor que esse valor. Ao controlar o tamanho do buffer, é preciso determinar o número de bursts necessários para preenchê-lo a fim de gerenciar a latência. Usar os métodos AAudioStreamBuilder_setBufferSizeInFrames() e AAudioStreamBuilder_getBufferSizeInFrames() para trabalhar com o tamanho do buffer.

Quando um aplicativo reproduz a saída de áudio, ele faz a gravação em um buffer e bloqueia até que a gravação seja concluída. A AAudio lê o buffer em bursts discretos. Cada burst contém um número de frames de áudio e geralmente é menor que o tamanho do buffer sendo lido. O sistema controla o tamanho e a taxa de bursts, essas propriedades geralmente são controladas pelos circuitos do dispositivo de áudio. Não é possível mudar o tamanho nem a taxa de bursts. No entanto, é possível definir o tamanho do buffer interno de acordo com o número de bursts que ele contém. Geralmente, uma latência mais baixa pode ser alcançada se o tamanho do buffer do AAudioStream for múltiplo do tamanho de burst indicado.

      Buffers da AAudio

Uma maneira de otimizar o tamanho do buffer é começar com um buffer grande, diminuí-lo gradualmente até que ocorram underruns e aumentar um pouco o tamanho dele novamente. Como alternativa, você pode começar com um buffer pequeno e aumentar o tamanho dele caso ocorram underruns até que a saída flua sem problemas novamente.

Esse processo pode ocorrer muito rapidamente, talvez até antes que o usuário reproduza o primeiro som. É recomendável fazer o dimensionamento inicial do buffer primeiro usando silêncio, para que o usuário não escute problemas de áudio. O desempenho do sistema pode mudar com o tempo (por exemplo, o usuário pode desativar o modo avião). Como o ajuste do buffer adiciona pouca sobrecarga, seu app pode fazer esse processo continuamente enquanto lê ou grava dados em um stream.

Veja este exemplo de repetição de otimização de 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);
        }
    }
}

Essa técnica não oferece vantagens para otimizar o tamanho do buffer de um stream de entrada. Os streams de entrada são executados o mais rápido possível, tentando minimizar a quantidade de dados em buffer e preenchendo-o quando o app é interrompido.

Usar um callback de alta prioridade

Caso seu aplicativo leia ou grave dados de áudio de uma linha de execução comum, talvez ele seja afetado ou sofra instabilidades na sincronização. Isso pode causar falhas de áudio. O uso de buffers maiores pode proteger contra essas falhas. No entanto, um buffer grande também aumenta a latência do áudio. Para aplicativos que exigem baixa latência, o stream de áudio pode usar uma função de callback assíncrona para transferir dados para dentro e para fora do aplicativo. A AAudio executa o callback em uma linha de execução de prioridade mais alta com melhor desempenho.

A função de callback tem este protótipo:

typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames);

Use a construção do stream para registrar o callback:

AAudioStreamBuilder_setDataCallback(builder, myCallback, myUserData);

No caso mais simples, o stream periodicamente executa a função de callback a fim de receber dados para o próximo burst.

A função de callback não deve ler ou gravar no stream que a invocou. Se o callback pertencer a um stream de entrada, seu código precisará processar os dados fornecidos no buffer audioData (especificado como o terceiro argumento). Se o callback pertencer a um stream de saída, seu código precisará colocar os dados no buffer.

Por exemplo, é possível usar um callback para gerar continuamente uma saída senoidal como esta:

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;
}

É possível processar mais de um stream usando a AAudio. Você pode usar um stream como mestre e passar ponteiros para outros streams nos dados do usuário. Registre um callback para o stream mestre. Em seguida, use E/S que não bloqueie nos outros streams. Veja um exemplo de callback de ida e volta que transmite um stream de entrada para outro de saída. O stream de chamada principal é o de saída. O stream de entrada é incluído nos dados do usuário.

O callback faz uma leitura sem bloqueio do stream de entrada colocando os dados no buffer do stream de saída:

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;
}

Neste exemplo, considera-se que os streams de entrada e saída têm o mesmo número de canais, formato e taxa de amostragem. O formato dos streams pode ser incompatível, desde que o código manipule as traduções corretamente.

Configuração do modo performance

Cada AAudioStream tem um modo de desempenho que afeta de forma significativa o comportamento do seu aplicativo. Existem três modos:

  • AAUDIO_PERFORMANCE_MODE_NONE é o modo padrão. Ele usa um stream básico que equilibra a latência e a economia de energia.
  • AAUDIO_PERFORMANCE_MODE_LOW_LATENCY usa buffers menores e um caminho de dados otimizado para reduzir a latência.
  • AAUDIO_PERFORMANCE_MODE_POWER_SAVING usa buffers internos maiores e um caminho de dados que reduz a latência para um menor consumo de energia.

Chame setPerformanceMode() para selecionar o modo performance e getPerformanceMode() para descobrir o modo atual.

Se a baixa latência for mais importante do que a economia de energia no seu aplicativo, use AAUDIO_PERFORMANCE_MODE_LOW_LATENCY. Esse modo é útil para aplicativos mais interativos, como jogos ou sintetizadores de teclado.

Se a economia de energia for mais importante que a baixa latência no aplicativo, use AAUDIO_PERFORMANCE_MODE_POWER_SAVING. Ele normalmente é usado para aplicativos que reproduzem músicas geradas anteriormente, como streaming de áudio ou players de arquivos MIDI.

Na versão atual da AAudio, para alcançar a menor latência possível, é necessário usar o modo performance AAUDIO_PERFORMANCE_MODE_LOW_LATENCY junto a um callback de alta prioridade. Siga este exemplo:

// 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);

Segurança de linha de execução

A API AAudio não é totalmente segura para linhas de execução. Algumas funções da AAudio não podem ser chamadas simultaneamente de mais de uma linha de execução por vez. Isso ocorre porque a AAudio evita o uso de mutexes, o que pode causar falhas e preempção de linhas de execução.

Por segurança, não chame AAudioStream_waitForStateChange() e não leia nem grave no mesmo stream a partir de duas linhas de execução diferentes. Da mesma forma, não feche um stream em uma linha de execução enquanto lê ou grava nele em outra linha de execução.

Chamadas que retornam configurações de stream, como AAudioStream_getSampleRate() e AAudioStream_getChannelCount(), são seguras para linhas de execução.

Estas chamadas também são seguras para linhas de execução:

  • AAudio_convert*ToText()
  • AAudio_createStreamBuilder()
  • AAudioStream_get*() exceto para AAudioStream_getTimestamp()

Problemas conhecidos

  • A latência de áudio é alta para bloquear write() porque a versão do Android O DP2 não usa uma faixa FAST. Use um callback para alcançar uma latência menor.

Outros recursos

Para saber mais, consulte os seguintes recursos:

Referência da API

Codelabs

Vídeos