Criar mais ondas - Sampler

Neste codelab, criaremos um sampler de áudio. O app grava o áudio com o microfone integrado do smartphone e o reproduz.

O app pode gravar até 10 segundos de áudio com o botão Gravar pressionado. Quando você tocar em Reproduzir, o áudio gravado será tocado uma vez enquanto você mantiver o botão pressionado. Também é possível ativar a Repetição, que reproduz a gravação sem parar até você soltar o botão Reproduzir. Sempre que você pressionar Gravar, a gravação de áudio anterior será substituída.

7eb653b71774dfed.png

O que você aprenderá

  • Conceitos básicos para criar um stream de gravação de baixa latência
  • Como armazenar e reproduzir dados de áudio gravados em um microfone

Pré-requisitos

Antes de iniciar este codelab, conclua o WaveMaker Parte 1. Ele aborda alguns conceitos básicos para criar streams de áudio que não são discutidos aqui.

O que é necessário

Nosso app de sampler tem quatro componentes:

  • IU: programada em Java, a classe MainActivity é responsável por receber eventos de toque e encaminhá-los para a ponte JNI.
  • Ponte JNI: este arquivo C++ usa JNI para fornecer um mecanismo de comunicação entre nossa IU e objetos C++. Ele encaminha eventos da IU para o mecanismo de áudio.
  • Mecanismo de áudio: esta classe C++ cria os streams de áudio de gravação e reprodução.
  • Gravação de som: esta classe C++ armazena os dados de áudio na memória.

Veja a arquitetura:

a37150c7e35aa3f8.png

Clonar o projeto

Clone o repositório do codelab no GitHub.

git clone https://github.com/googlecodelabs/android-wavemaker2

Importar o projeto para o Android Studio

Abra o Android Studio e importe o projeto:

  • File -> New -> Import project….
  • Escolha a pasta android-wavemaker2.

Executar o projeto

Escolha a configuração de execução básica.

f65428e71e9bdbcf.png

Em seguida, pressione Ctrl+R para criar e executar o app modelo. Ele será compilado e executado, mas não funcionará. A funcionalidade dele será adicionada durante este codelab.

Abrir o módulo base

Os arquivos com que você trabalhará neste codelab são armazenados no módulo base. Expanda o módulo na janela Project e verifique se a visualização Android está selecionada.

cae7ee7b54407790.png

Observação: o código-fonte completo do app WaveMaker2 pode ser encontrado no módulo final.

O objeto SoundRecording representa os dados de áudio gravados na memória. Ele permite que o app grave dados do microfone na memória e os leia na reprodução.

Para começar, vejamos como armazenar esses dados de áudio.

Escolher um formato de áudio

Primeiro, precisamos escolher um formato de áudio para nossas amostras. A AAudio é compatível com dois formatos:

  • float: ponto flutuante de precisão única (4 bytes por amostra)
  • int16_t: números inteiros de 16 bits (2 bytes por amostra)

Para termos uma boa qualidade de som em volume baixo e por outros motivos, usamos amostras float. Se a memória for um problema, sacrifique a fidelidade e use números inteiros de 16 bits para ganhar espaço.

Escolher a quantidade de armazenamento necessária

Vamos supor que queremos armazenar 10 segundos de dados de áudio. Com uma taxa de amostragem de 48.000 amostras por segundo (a taxa mais comum em dispositivos Android modernos), precisamos alocar memória para 480.000 amostras.

Abra base/cpp/SoundRecording.h e defina essa constante na parte superior do arquivo.

constexpr int kMaxSamples = 480000; // 10s of audio data @ 48kHz

Definir a matriz de armazenamento

Agora temos todas as informações necessárias para definir uma matriz de floats. Adicione a seguinte declaração a SoundRecording.h:

private:
    std::array<float,kMaxSamples> mData { 0 };

O { 0 } usa a inicialização agregada para definir todos os valores na matriz como 0.

Implementar write

O método write tem a seguinte assinatura:

int32_t write(const float *sourceData, int32_t numFrames);

Esse método recebe uma matriz de amostras de áudio em sourceData. O tamanho da matriz é especificado por numFrames. O método retornará o número de amostras efetivamente gravadas.

Isso pode ser implementado armazenando o próximo índice de gravação disponível. Inicialmente, é 0:

9b3262779d7a0a8c.png

Depois que cada amostra é gravada, o índice de gravação seguinte continua por um:

2971acee93b9869d.png

Isso pode ser facilmente implementado como uma repetição for. Adicione o seguinte código ao método write em SoundRecording.cpp

for (int i = 0; i < numSamples; ++i) {
    mData[mWriteIndex++] = sourceData[i];
}
return numSamples;

Mas e se não tivermos espaço para todas as amostras que queremos gravar? Problemas acontecem. Ocorrerá uma falha de segmentação causada pela tentativa de exceder os limites do índice de matriz.

Vamos adicionar uma verificação que mude numSamples caso mData não tenha espaço suficiente. Adicione o seguinte código acima do existente:

if (mWriteIndex + numSamples > kMaxSamples) {
    numSamples = kMaxSamples - mWriteIndex;
}

Implementar read

O método read é semelhante a write. Armazenamos o índice de leitura seguinte.

488ab2652d0d281d.png

E o incrementamos após a leitura de uma amostra.

1a7fd22f4bbb4940.png

Repetimos isso até que o número solicitado de amostras seja lido. O que acontece quando chegamos ao fim dos dados disponíveis? Temos dois comportamentos:

  • Se a repetição estiver ativada, o índice de leitura será definido como zero, ou seja, o início da matriz de dados.
  • Se a repetição estiver desativada, nada será feito. O índice de leitura não será incrementado.

789c2ce74c3a839d.png

Nesses dois comportamentos, precisamos saber quando chegamos ao fim dos dados disponíveis. Para nossa sorte, mWriteIndex vai mostrar exatamente isso. Ele contém o número total de amostras gravadas na matriz de dados.

Considerando isso, podemos implementar o método read em SoundRecording.cpp

int32_t framesRead = 0;
while (framesRead < numSamples && mReadIndex < mWriteIndex){
    targetData[framesRead++] = mData[mReadIndex++];
    if (mIsLooping && mReadIndex == mWriteIndex) mReadIndex = 0;
}
return framesRead;

O AudioEngine executa as seguintes tarefas principais:

  • Criar uma instância de SoundRecording
  • Criar um stream de gravação para gravar dados do microfone
  • Gravar os dados na instância de SoundRecording no callback do stream de gravação
  • Criar um stream de reprodução para reproduzir os dados gravados
  • Ler os dados na instância de SoundRecording no callback do stream de reprodução
  • Responder a eventos de IU para gravação, reprodução e repetição

Vamos começar a criar a instância SoundRecording.

Comece com algo fácil. Crie uma instância de SoundRecording no AudioEngine.h:

private:
    SoundRecording mSoundRecording;

Precisamos criar dois streams: de reprodução e de gravação. Qual devemos criar primeiro?

O stream de reprodução, porque ele tem apenas uma taxa de amostragem que oferece a menor latência. Em seguida, podemos fornecer a taxa de amostragem ao criador do stream de gravação. Isso garante que os streams de reprodução e gravação tenham a mesma taxa de amostragem, ou seja, não será necessário fazer reamostragem entre eles.

Propriedades do stream de reprodução

Use estas propriedades para preencher o StreamBuilder que criará o stream de reprodução:

  • Direção: não especificada, o padrão é a saída
  • Taxa de amostragem: não especificada, o padrão é a taxa com menor latência
  • Formato: ponto flutuante
  • Contagem de canais: 2 (estéreo)
  • Modo de desempenho: baixa latência
  • Modo de compartilhamento: exclusivo

Criar o stream de reprodução

Agora, temos tudo que precisamos para criar e abrir o stream de reprodução. Adicione o seguinte código à parte superior do método start em AudioEngine.cpp.

// Create the playback stream.
StreamBuilder playbackBuilder = makeStreamBuilder();
AAudioStreamBuilder_setFormat(playbackBuilder.get(), AAUDIO_FORMAT_PCM_FLOAT);
AAudioStreamBuilder_setChannelCount(playbackBuilder.get(), kChannelCountStereo);
AAudioStreamBuilder_setPerformanceMode(playbackBuilder.get(), AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setSharingMode(playbackBuilder.get(), AAUDIO_SHARING_MODE_EXCLUSIVE);
AAudioStreamBuilder_setDataCallback(playbackBuilder.get(), ::playbackDataCallback, this);
AAudioStreamBuilder_setErrorCallback(playbackBuilder.get(), ::errorCallback, this);

aaudio_result_t result = AAudioStreamBuilder_openStream(playbackBuilder.get(), &mPlaybackStream);

if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error opening playback stream %s",
                       AAudio_convertResultToText(result));
   return;
}

// Obtain the sample rate from the playback stream so we can request the same sample rate from
// the recording stream.
int32_t sampleRate = AAudioStream_getSampleRate(mPlaybackStream);

result = AAudioStream_requestStart(mPlaybackStream);
if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error starting playback stream %s",
                       AAudio_convertResultToText(result));
   closeStream(&mPlaybackStream);
   return;
}

Observe que foram criados modelos de método para os callbacks de dados e de erro. Se precisar rever como eles funcionam, consulte o primeiro codelab.

Gravar propriedades de stream

Use estas propriedades para criar o stream de gravação:

  • Direção: entrada (gravação é uma entrada, enquanto reprodução é uma saída)
  • Taxa de amostragem: igual ao stream de saída
  • Formato: ponto flutuante
  • Contagem de canais: 1 (mono)
  • Modo de desempenho: baixa latência
  • Modo de compartilhamento: exclusivo

Criar o stream de gravação

Agora, adicione o código a seguir abaixo do código adicionado anteriormente em start.

// Create the recording stream.
StreamBuilder recordingBuilder = makeStreamBuilder();
AAudioStreamBuilder_setDirection(recordingBuilder.get(), AAUDIO_DIRECTION_INPUT);
AAudioStreamBuilder_setPerformanceMode(recordingBuilder.get(), AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setSharingMode(recordingBuilder.get(), AAUDIO_SHARING_MODE_EXCLUSIVE);
AAudioStreamBuilder_setFormat(recordingBuilder.get(), AAUDIO_FORMAT_PCM_FLOAT);
AAudioStreamBuilder_setSampleRate(recordingBuilder.get(), sampleRate);
AAudioStreamBuilder_setChannelCount(recordingBuilder.get(), kChannelCountMono);
AAudioStreamBuilder_setDataCallback(recordingBuilder.get(), ::recordingDataCallback, this);
AAudioStreamBuilder_setErrorCallback(recordingBuilder.get(), ::errorCallback, this);

result = AAudioStreamBuilder_openStream(recordingBuilder.get(), &mRecordingStream);

if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error opening recording stream %s",
                       AAudio_convertResultToText(result));
   closeStream(&mRecordingStream);
   return;
}

result = AAudioStream_requestStart(mRecordingStream);
if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error starting recording stream %s",
                       AAudio_convertResultToText(result));
   return;
}

Agora vamos à parte divertida: armazenar os dados gravados do microfone no objeto SoundRecording.

Quando criamos o stream de gravação, especificamos o callback de dados como ::recordingDataCallback. Esse método chama AudioEngine::recordingCallback, que tem a seguinte assinatura:

aaudio_data_callback_result_t AudioEngine::recordingCallback(float *audioData,
                                                            int32_t numFrames)

Os dados de áudio são fornecidos em audioData.. O tamanho (em amostras) é de numFrames porque há apenas uma amostra por frame, já que estamos gravando em mono.

Só é preciso fazer o seguinte:

  • Verifique mIsRecording para ver se estamos gravando.
  • Se não estivermos, ignore os dados recebidos.
  • Se estivermos gravando, siga estas etapas:
  • Forneça audioData a SoundRecording usando o método write.
  • Verifique o valor de retorno de write. Caso seja zero, SoundRecording está cheio e a gravação precisa ser interrompida.
  • Retorne AAUDIO_CALLBACK_RESULT_CONTINUE para que os callbacks continuem.

Adicione o código a seguir a recordingCallback:

if (mIsRecording) {
    int32_t framesWritten = mSoundRecording.write(audioData, numFrames);
    if (framesWritten == 0) mIsRecording = false;
}
return AAUDIO_CALLBACK_RESULT_CONTINUE;

Assim como o stream de gravação, o stream de reprodução chama playbackDataCallback quando precisa de novos dados. Esse método chama AudioEngine::playbackCallback,, que tem a seguinte assinatura:

aaudio_data_callback_result_t AudioEngine::playbackCallback(float *audioData, int32_t numFrames)

Dentro desse método, precisamos fazer o seguinte:

  • Preencher a matriz com zeros usando fillArrayWithZeros.
  • Se estivermos reproduzindo os dados gravados, indicados por mIsPlaying, precisamos fazer o seguinte:
  • Ler numFrames de dados de mSoundRecording.
  • Converter audioData de mono para estéreo usando convertArrayMonoToStereo.
  • Se o número de frames lidos for menor que o número de frames solicitados, teremos chegado ao fim dos dados gravados. Para interromper a reprodução, defina mIsPlaying como false.

Adicione o código a seguir a playbackCallback:

fillArrayWithZeros(audioData, numFrames * kChannelCountStereo);

if (mIsPlaying) {
   int32_t framesRead = mSoundRecording.read(audioData, numFrames);
   convertArrayMonoToStereo(audioData, framesRead);
   if (framesRead < numFrames) mIsPlaying = false;
}
return AAUDIO_CALLBACK_RESULT_CONTINUE;

Iniciar e interromper a gravação

O método setRecording é usado para iniciar e interromper a gravação. Veja a assinatura dele:

void setRecording(bool isRecording)

Quando o botão de gravação é pressionado, isRecording é true. Quando o botão é liberado, isRecording é false.

Uma variável de membro mIsRecording é usada para alternar o estado do armazenamento de dados em recordingCallback. Só precisamos definir o valor de isRecording.

Precisamos adicionar mais um comportamento. Quando a gravação é iniciada, todos os dados existentes em mSoundRecording precisam ser substituídos. Isso pode ser feito usando clear, que redefinirá o índice de gravação de mSoundRecording como zero.

Veja o código para setRecording:

if (isRecording) mSoundRecording.clear();
mIsRecording = isRecording;

Iniciar e interromper a reprodução

O controle de reprodução é semelhante ao de gravação. setPlaying é chamado quando o botão de reprodução é pressionado ou liberado. A variável de membro mIsPlaying alterna o estado da reprodução em playbackCallback.

Quando o botão de reprodução for pressionado, o ideal é que a reprodução seja iniciada no começo dos dados de áudio gravados. Para isso, use setReadPositionToStart, que redefine o cabeçalho de leitura de mSoundRecording para zero.

Veja o código para setPlaying:

if (isPlaying) mSoundRecording.setReadPositionToStart();
mIsPlaying = isPlaying;

Ativar a repetição da reprodução

Por fim, quando a chave LOOP é ativada, setLooping é chamado. Para processar isso, transmita o argumento isOn para mSoundRecording.setLooping:

Veja o código para setLooping:

mSoundRecording.setLooping(isOn);

Os apps que gravam áudio precisam solicitar a permissão RECORD_AUDIO do usuário. Grande parte do código de processamento de permissões já foi programada. No entanto, ainda precisamos declarar que nosso app usa essa permissão.

Adicione a seguinte linha a manifests/AndroidManifest.xml na seção <manifest>:

<uses-permission android:name="android.permission.RECORD_AUDIO" />

Chegou a hora de ver se deu tudo certo. Crie e execute o app. Você verá a seguinte IU.

7eb653b71774dfed.png

Toque no botão vermelho para iniciar a gravação. Ela continuará enquanto você mantiver o botão pressionado, até um máximo de 10 segundos. Toque no botão verde para reproduzir os dados gravados. A reprodução continuará enquanto você mantiver o botão pressionado até o final dos dados de áudio. Se o LOOP estiver ativado, os dados de áudio serão repetidos indefinidamente.

Leia mais

Amostras de áudio de alto desempenho (link em inglês).

Guia de áudio de alto desempenho na documentação do Android NDK.

Vídeo de Práticas recomendadas para áudio no Android - Google I/O 2017 (em inglês).