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.
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
- Android Studio 3.0.0 ou mais recente
- SDK do Android 8.0 (API de nível 26)
- NDK e Build Tools instalados
- Um simulador ou dispositivo Android com o Android 8.0 (API de nível 26) ou mais recente para testes
- Ter algum conhecimento de C++ é útil, mas não é obrigató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:
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.
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.
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 float
s. 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:
Depois que cada amostra é gravada, o índice de gravação seguinte continua por um:
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.
E o incrementamos após a leitura de uma amostra.
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.
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
aSoundRecording
usando o métodowrite
. - 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 demSoundRecording
. - Converter
audioData
de mono para estéreo usandoconvertArrayMonoToStereo
. - 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
comofalse
.
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.
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).