Continuando nossa série de artigos traduzidos do site lazyfoo, veremos agora como realizar a gravação de áudio com a biblioteca SDL.
Em relação a áudio, existem mais coisas que podem ser feitas do que somente reproduzi-lo. Esse artigo irá cobrir o básico sobre gravação e reprodução de áudio.
Para começar, certifique-se de possuir a última versão do SDL. Eu tive que atualizar para a versão 2.0.8 para que a gravação de áudio funcionasse direito. Para entender como gravação de áudio funciona, é de grande ajuda saber como dados de áudio funcionam.
Isso é uma música:
Agora vamos dar um zoom:
Como você pode ver, som é uma onda. Uma onda pode ser representada por uma sequência de valores (ou no caso de uma música estéreo, por duas sequências com um valor para cada onda). Reproduzir som é basicamente enviar uma sequência de valores para o driver de som e gravação de áudio é copiar uma sequência de valores do driver de som.
//Maximum number of supported recording devices
const int MAX_RECORDING_DEVICES = 10;
//Maximum recording time
const int MAX_RECORDING_SECONDS = 5;
//Maximum recording time plus padding
const int RECORDING_BUFFER_SECONDS = MAX_RECORDING_SECONDS + 1;
//The various recording actions we can take
enum RecordingState
{
SELECTING_DEVICE,
STOPPED,
RECORDING,
RECORDED,
PLAYBACK,
ERROR
};
Aqui tem algumas constantes que estaremos usando.
Em primeiro lugar, temos a constante que define que suportaremos no máximo 10 dispositivos de gravação (nós precisamos apenas de 1 para que o programa funcione). Em seguida temos a quantidade máxima de tempo que permitiremos que seja gravado e a quantidade de tempo máxima que podemos armazenar no buffer. Nós estaremos gravando por 5 segundos, mas permitiremos 6 segundos de gravação por segurança, no caso da aplicação gravar 5.1 segundo ou algo do tipo.
Por último, temos uma enumeração para os diferentes estados do programa. Primeiro o usuário seleciona um dispositivo de gravação. Em seguida, ficamos aguardando o inicio da gravação. Depois o usuário inicia a gravação por 5 segundos. Quando a gravação é finalizada, pode-se ou reproduzir o áudio ou iniciar nova gravação. Se ocorrer um erro, iremos para um estado de erro.
Normalmente nessa série de artigos, vamos percorrendo um código explicando o que cada parte faz, mas nesse artigo iremos seguir mais o fluxo de execução. Isso torna as coisas mais fáceis de entender do que somente seguindo o código fonte. Assim, não se perca a medida que seguimos.
//Recording/playback callbacks
void audioRecordingCallback( void* userdata, Uint8* stream, int len );
void audioPlaybackCallback( void* userdata, Uint8* stream, int len );
Essas duas funções irão executar a gravação e reprodução do áudio. Iremos mostrar mais detalhes delas mais tarde.
//Prompt texture
LTexture gPromptTexture;
//The text textures that specify recording device names
LTexture gDeviceTextures[ MAX_RECORDING_DEVICES ];
//Number of available devices
int gRecordingDeviceCount = 0;
//Recieved audio spec
SDL_AudioSpec gReceivedRecordingSpec;
SDL_AudioSpec gReceivedPlaybackSpec;
Aqui temos algumas texturas. Uma para mostrar ao usuário o que está acontecendo e outra é um array de texturas para armazenar os nomes dos dispositivos de gravação. Também temos um inteiro para registro de quantos dispositivos estão disponíveis
Também temos duas variáveis SDL_AudioSpec. Esse tipo de variável é uma especificação de áudio que basicamente define como o áudio é gravado ou reproduzido. Quando abrimos um dispositivo de áudio para gravação ou reprodução, uma especificação é solicitada mas podemos não obter o que foi solicitado, pois o driver de áudio não suporta a especificação. Por isso que iremos armazenar a especificação que obtermos do driver para a gravação e a reprodução.
//Recording data buffer
Uint8* gRecordingBuffer = NULL;
//Size of data buffer
Uint32 gBufferByteSize = 0;
//Position in data buffer
Uint32 gBufferBytePosition = 0;
//Maximum position in data buffer for recording
Uint32 gBufferByteMaxPosition = 0;
O “gRecordingBuffer” é um buffer de bytes sem sinal que armazena os dados do áudio. “gBufferByteSize” irá armazenar quantos bytes o buffer irá armazenar. “gBufferBytePosition” controla a posição do buffer durante a gravação e/ou a reprodução. “gBufferByteMaxPosition” controla a parte máxima do buffer sendo usado.
Se isso está confuso, lembre que “gBufferByteSize” contém 6 segundos de bytes (5 segundos + 1 byte de espaçamento) e “gBufferByteMaxPosition” é o quinto segundo de bytes sendo usados.
Após carregar a fonte e renderizar a mensagem inicial obtemos o numero de dispositivos de gravação disponíveis usando SDL_GetNumAudioDevices. Quando o valor SDL_TRUE é passado, será retornado o numero de dispositivos de gravação. Com SDL_FALSE, será retornado o numero de dispositivos de reprodução.
Se não houver nenhum dispositivo de gravação conectado, a função é encerrada com um erro.
//At least one device connected
else
{
//Cap recording device count
if( gRecordingDeviceCount > MAX_RECORDING_DEVICES )
{
gRecordingDeviceCount = MAX_RECORDING_DEVICES;
}
//Render device names
std::stringstream promptText;
for( int i = 0; i < gRecordingDeviceCount; ++i )
{
//Get capture device name
promptText.str( "" );
promptText << i << ": " << SDL_GetAudioDeviceName( i, SDL_TRUE );
//Set texture from name
gDeviceTextures[ i ].loadFromRenderedText( promptText.str().c_str(), gTextColor );
}
}
}
return success;
}
Se houver dispositivos de gravação conectados, cortamos a quantidade para o máximo de 10 (o que pode disapontar quem tem 11 microfones conectados ao PC) e então percorremos cada um e renderizamos seus nomes em uma textura. Obtemos o nome do dispositivo usando SDL_GetAudioDeviceName usando como parametros SDL_TRUE para indicar que queremos o nome de um dispositivo de gravação e o indice do dispositivo.
Na função main, depois da inicialização e do carregamento, configuramos o estado de gravação inicial e declaramos dois IDs de dispositivo de áudio que são apenas inteiros que representam dispositivos de gravação e reprodução.
//Do current state event handling
switch( currentState )
{
//User is selecting recording device
case SELECTING_DEVICE:
//On key press
if( e.type == SDL_KEYDOWN )
{
//Handle key press from 0 to 9
if( e.key.keysym.sym >= SDLK_0 && e.key.keysym.sym <= SDLK_9 )
{
//Get selection index
int index = e.key.keysym.sym - SDLK_0;
No loop de manipulação de eventos, temos uma sentença switch que lida com os diferentes estados. Quando o usuário pressiona 0-9, convertemos esse numero para um indíce, o que é fácil pois as contantes SDLK são sequenciais e podem ser convertidas pela subtração do simbolo por SDLK_0.
Se o usuário pressionar uma tecla válida, passamos para a especificação da gravação do áudio.
Primeiro, inicializamos a especificação do ´áudio com SDL_zero. Sempre inicialize a memória antes de usa-la. Pergunte a quem teve que lidar com bugs relacionados a isso o que acontece se você não fizer isso.
Configuramos a frequência para 44.1 khz, que é a qualidade de um CD. Usamos o formato de ponto flutuante de 32 bits para os dados. Temos 2 canais já que queremos estéreo. Amostragem é configurada para 4096 pois é um tamanho bem padrão. Em seguida informamos a função a ser chamada para gravar o áudio.
Com a especificação configurada, chamados SDL_OpenAudioDevice informando o nome do dispositivo de gravação, indicamos que iremos usar um dispositivo de gravação com SDL_TRUE, a especificação que iremos usar, um ponteiro para receber a especificação recebida do driver, e por fim um indicador que informa que SDL_OpenAudioDevice pode retornar um formato diferente do que foi especificado.
Se não obtivermos nenhum ID de dispositivo, temos um estado de erro. Se o dispositivo for aberto com sucesso, criamos uma especificação de reprodução que é praticamente a mesma da de gravação. A maior difereça é que usa função de reprodução ao invés da de gravação.
Abri o dispositivo de reprodução é basicamente a mesma coisa. Nesse artigo, não nos importamos que dispositivo de reprodução iremos obter,k assim usamos NULL como argumento para que seja retornado o primeiro disponível. Em seguida, usamos SDL_FALSE para abrir um dispositivo de reprodução ao invés de um de gravação.
//Device failed to open
if( playbackDeviceId == 0 )
{
//Report error
printf( "Failed to open playback device! SDL Error: %s", SDL_GetError() );
gPromptTexture.loadFromRenderedText( "Failed to open playback device!", gTextColor );
currentState = ERROR;
}
//Device opened successfully
else
{
//Calculate per sample bytes
int bytesPerSample = gReceivedRecordingSpec.channels * ( SDL_AUDIO_BITSIZE( gReceivedRecordingSpec.format ) / 8 );
//Calculate bytes per second
int bytesPerSecond = gReceivedRecordingSpec.freq * bytesPerSample;
//Calculate buffer size
gBufferByteSize = RECORDING_BUFFER_SECONDS * bytesPerSecond;
//Calculate max buffer use
gBufferByteMaxPosition = MAX_RECORDING_SECONDS * bytesPerSecond;
//Allocate and initialize byte buffer
gRecordingBuffer = new Uint8[ gBufferByteSize ];
memset( gRecordingBuffer, 0, gBufferByteSize );
//Go on to next state
gPromptTexture.loadFromRenderedText("Press 1 to record for 5 seconds.", gTextColor);
currentState = STOPPED;
}
}
}
}
}
break;
Se não obtivermos um ID de dispositivo para reprodução, um erro será disparado. Se o dispositivo for aberto com sucesso, criamos um buffer para armazenar os dados do áudio que iremos gravar para ser reproduzido em seguida.
Para calcular quanto espaço será necessário primeiro precisamos calcular quantos bytes termos por amostra. Se temos 2 canais e 32 bits por canal (que podemos saber usando SDL_AUDIO_BITSIZE no formato do áudio) teremos então 2 canais * (32 bits / 8 bits por byte) que dá 8 bytes por amostra.
Para obter a quantidade de bytes por segundo, multiplicamos a quantidade de bytes por amostra pela frequência, que é a quantidade de amostras por segundo. 8 bytes por amostra * 44100 amostras por segundo dá 705600 bytes por segundo.
Queremos 6 segundos de buffer (5 segundos + 1 segundo de espaçamentp), então configuramos o tamanho do buffer para 4233600 bytes. Isso parece muito, mas é um pouco mais de 4 megabytes. Lembre de que por causa da posiçã omáxima, usamos apenas 5 segundos dos 6 segundos do buffer.
Após calcular o tamanho do buffer, alocamos o buffer e inicializamos ele com memset. Finalmente, ajustamos a textura e passamos para o próximo estado.
//User getting ready to record
case STOPPED:
//On key press
if( e.type == SDL_KEYDOWN )
{
//Start recording
if( e.key.keysym.sym == SDLK_1 )
{
//Go back to beginning of buffer
gBufferBytePosition = 0;
//Start recording
SDL_PauseAudioDevice( recordingDeviceId, SDL_FALSE );
//Go on to next state
gPromptTexture.loadFromRenderedText( "Recording...", gTextColor );
currentState = RECORDING;
}
}
break;
Após alocarmos o bufer, estamos prontos para começar a gravação. Se o usuário pressiona 1, ajustamos a posição do buffer para 0 e iniciamos o dispositivo de áudio usando SDL_PauseAudioDevice. O primeiro parametro é o dispositivo que queremos iniciar/pausar e o segundo argumento determina se queremos iniciar ou pausar. Usando SDL_FALSE iremos iniciar o dispositivo.
Dispositivos de áudio poir padrão ficam pausados, o que significa que eles não irão gravar ou reproduzir áudio até serem iniciados. Se você está se perguntando se a sua função não está sendo executada, pode ser por isso.
void audioRecordingCallback( void* userdata, Uint8* stream, int len )
{
//Copy audio from stream
memcpy( &gRecordingBuffer[ gBufferBytePosition ], stream, len );
//Move along buffer
gBufferBytePosition += len;
}
Quando o dispositivo de gravação é iniciado, começará a chamar a função de gravação que fornecemos em intervalos regulares.
Como você pode ver, ela não faz muita coisa. Tudo que ela faz é copiar alguns bytes do stream do dispositivo para a posição atual de nosso buffer de gravação e então altera a posição atual do buffer. Isso é tudo que a gravação é, apenas pegar pedaços de dados de áudio. Apenas lembre-se que “len” é o tamanho do pedaço do stream de bytes.
//Updating recording
if( currentState == RECORDING )
{
//Lock callback
SDL_LockAudioDevice( recordingDeviceId );
//Finished recording
if( gBufferBytePosition > gBufferByteMaxPosition )
{
//Stop recording audio
SDL_PauseAudioDevice( recordingDeviceId, SDL_TRUE );
//Go on to next state
gPromptTexture.loadFromRenderedText( "Press 1 to play back. Press 2 to record again.", gTextColor );
currentState = RECORDED;
}
//Unlock callback
SDL_UnlockAudioDevice( recordingDeviceId );
}
Aqui pulamos para a parte de atualização do loop. Quando estamos gravando, precisamos verificar se atingimos os 5 segundos do buffer. Antes de podermos checar a posição no buffer, temos que chamar SDL_LockAudioDevice. Isso se deve ao fato da função de gravação estar sendo executada em uma outra thread e não queremos que duas thread acesses a mesma variável ao mesmo tempo. SDL_LockAudioDevice interrompe a função de gravação enquanto temos que acessar a posição no buffer que a função também manipula.
Assim que o dispositivo estiver bloqueado, verificamos se a posição no buffer ultrapassou os 5 segundos de dados. Se tiver ultrapassado, pausamos o dispositivo de gravação para interromper a gravação e passarmos para outro estado. Por último, chamamos SDL_UnlockAudioDevice caso ainda tenhamos dados para gravar de modo que o dispositivo de gravação possa continuar.
Isso é um exemplo bem simples de multithreading. Se quiser saber mais sobre o assunto, dê um olhada nos artigos dessa série sobre multithreading, semáforos e mutexes.
//User has finished recording
case RECORDED:
//On key press
if( e.type == SDL_KEYDOWN )
{
//Start playback
if( e.key.keysym.sym == SDLK_1 )
{
//Go back to beginning of buffer
gBufferBytePosition = 0;
//Start playback
SDL_PauseAudioDevice( playbackDeviceId, SDL_FALSE );
//Go on to next state
gPromptTexture.loadFromRenderedText( "Playing...", gTextColor );
currentState = PLAYBACK;
}
Aqui voltamos para a manipulação de eventos depois de ter gravado os 5 segundos.
Como você pode ver, é similar a quando começamos a gravação. Ajustamos a posição no buffer para o inicio, iniciamos o dispositivo de reprodução e mudamos para o estado.
void audioPlaybackCallback( void* userdata, Uint8* stream, int len )
{
//Copy audio to stream
memcpy( stream, &gRecordingBuffer[ gBufferBytePosition ], len );
//Move along buffer
gBufferBytePosition += len;
}
A função de reprodução também é similar à função de gravação. A diferença chave aqui é que ao invés de copiar do dispositivo para o buffer, pegamos os dados que gravamos no buffer e copiamos de volta para o dispositivo.
//Updating playback
else if( currentState == PLAYBACK )
{
//Lock callback
SDL_LockAudioDevice( playbackDeviceId );
//Finished playback
if( gBufferBytePosition > gBufferByteMaxPosition )
{
//Stop playing audio
SDL_PauseAudioDevice( playbackDeviceId, SDL_TRUE );
//Go on to next state
gPromptTexture.loadFromRenderedText( "Press 1 to play back. Press 2 to record again.", gTextColor );
currentState = RECORDED;
}
//Unlock callback
SDL_UnlockAudioDevice( playbackDeviceId );
}
Atualizar a reprodução também é similar à gravação. Bloqueamos o dispositivo de reprodução, verificamos a posição da reprodução, paramos a reprodução se a posição no buffer estiver além da posição final, e desbloquamos o dispositivo de reprodução.
//Record again
if( e.key.keysym.sym == SDLK_2 )
{
//Reset the buffer
gBufferBytePosition = 0;
memset( gRecordingBuffer, 0, gBufferByteSize );
//Start recording
SDL_PauseAudioDevice( recordingDeviceId, SDL_FALSE );
//Go on to next state
gPromptTexture.loadFromRenderedText( "Recording...", gTextColor );
currentState = RECORDING;
}
}
break;
}
Pulando de volta para a manipulação de eventos após o final da gravação, permitimos que o usuário possa iniciar nova gravação. Quando quisermos iniciar nova gravação, apenas ajustamos a posição no buffer para o início, inicializamos o buffer e iniciamos o dispositivo de gravação.
Como sempre, não esqueça de desalocar o buffer depois de usa-lo para prevenir vazamentos de memória.
Programação com áudio é uma campo muito grande que eu não sou expert. Porém, agora que você sabe como lidar com dados brutos de áudio, pode ver como usar bibliotecas de áudio para fazer coisas mais complicadas como compressão de áudio e char por voz.
Baixe os arquivos de mídia e código fonte desse artigo aqui.