Processamento de áudio

Video deste projeto

Este projeto consiste em criar filtros para arquivos de áudio em formato WAV, implementando operações como ajuste de volume, eco e normalização.

Áudio digital

Em um sistema digital, um som é normalmente codificado como um vetor de amostras (samples), onde cada amostra corresponde à amplitude do sinal sonoro em um instante de tempo. As figuras a seguir trazem um exemplos de sinais sonoros codificados dessa forma:

Trecho de áudio (duração 45 s):

 Trecho de música (45 s)

Detalhe do trecho de áudio acima (duração 300 ms):

 Detalhe (300 ms)

Detalhe do trecho de áudio acima (duração 30 ms):

 Detalhe do detalhe (30 ms)

Detalhe do trecho de áudio acima (duração 0,3 ms):

 Detalhe do detalhe (0,3 ms)

Visão conceitual da amostragem de áudio:

Amostragem de sinal analógico

Nessa figura podem ser identificados o sinal analógico, proveniente de um microfone ou outro dispositivo de captura, e as amostras, que correspondem ao valor do sinal medido em intervalos regulares. O sinal analógico acima seria então representado de forma aproximada pelo seguinte vetor de amostras:

[0, 10, 15, 9, -5, -9, -10, 0, 10, 14, 15, 5, 0, -5, ... ]

O padrão PCM (Pulse-Code Modulation) é um dos padrões mais simples para representar áudio em meio digital. Duas informações caracterizam um sinal codificado em PCM:

No caso de sons com vários canais (estéreo ou surround), deve existir uma sequência de amostras para cada canal de som, geralmente todas usando a mesma taxa de amostragem e resolução.

O formato WAV

Para facilitar a leitura e escrita dos arquivos de áudio, neste projeto será adotado o formato WAV, reconhecido pela maioria dos softwares de geração e reprodução de áudio. Para este projeto será adotado o padrão WAV com codificação PCM de 16 bits com sinal. Neste formato, cada amostra é representada por um número inteiro de 16 bits com sinal (ou seja, do tipo int16_t, variando entre -32768 e +32767).

Um arquivo WAV típico é organizado nos seguintes chunks (pedaços) básicos:

Chunk ID tamanho (bytes) conteúdo
RIFF 12 cabeçalho RIFF (Resource Interchange File Format)
fmt 24 informações sobre o formato do áudio
data variável amostras do(s) canal(is) de áudio
Em alguns arquivos WAV podem haver outros chunks, como LIST e INFO, que não precisam ser considerados neste projeto.

Esses chunks têm os seguintes campos internos:

Chunk nome tamanho tipo valor significado
RIFF ChunkID 4 char[4] “RIFF” constante, identifica o tipo de arquivo
ChunkSize 4 uint32_t filesize - 8 tamanho do arquivo em bytes, sem considerar ChunkID e ChunkSize
Format 4 char[4] “WAVE” constante, define o formato do restante do conteúdo
fmt SubChunk1ID 4 char[4] “fmt ” constante, cabeçalho do chunk
SubChunk1Size 4 uint32_t 16 (PCM) tamanho deste chunk
Audio format 2 uint16_t 1 (PCM) codificação utilizada
Number of channels 2 uint16_t 1, 2, … número de canais de áudio
Sample rate 4 uint32_t 44100, etc Taxa de amostragem (amostras/seg) por canal
Byte rate 4 uint32_t varia taxa de bytes por segundo
Block align 2 uint16_t varia número de bytes por amostra (soma todos os canais)
Bits per sample 2 uint16_t 8, 16, 32, … bits por amostra, por canal
data SubChunk2ID 4 char[4] “data” constante, cabeçalho do chunk
SubChunk2Size 4 uint32_t varia espaço ocupado pelas amostras, em bytes
Audio data variável - - amostras de áudio
Observe que os campos de tipo char[4] não são strings, pois strings precisam ser terminadas por \0, o que não ocorre aqui.

As amostras de áudio são armazenadas no chunk de dados (data) do arquivo, em sequência por canal e por tempo. Cada amostra tem um tamanho fixo em bytes, definido pelo campo bits per sample do cabeçalho. Considerando um áudio com dois canais (Left e Right), as amostras estarão dispostas da seguinte forma:

L[0], R[0], L[1], R[1], L[2], R[2], L[3], R[3], ...
Para ler facilmente o cabeçalho de um arquivo WAV, basta definir um struct com os mesmos campos, tamanhos e ordem do cabeçalho descrito acima, e então ler os primeiros bytes do arquivo para dentro desse struct, usando a função de leitura fread. Uma vez lido o struct, basta verificar se os campos constantes têm os valores esperados.

Documentação adicional sobre o formato WAV:

Alguns exemplos de arquivos de áudio WAV para usar no projeto:

Você pode gerar seus próprios arquivos WAV usando programas de processamento de áudio, como o “Audacity”, Sox, etc.

Filtros

Um filtro de áudio é um programa simples, que recebe como entrada um arquivo de áudio (que pode vir da entrada padrão stdin), realiza algum tipo de processamento de áudio e entrega na saída um arquivo de áudio (que pode ser stdout). Eventuais mensagens de erro devem ser enviadas para a saída de erro (stderr).

Lembre-se que o filtro deve atuar em todos os canais do áudio da entrada.

Informações

Este programa não é exatamente um filtro, pois produz como saída uma listagem das principais informações do áudio informado como entrada.

Forma de chamada:

wavinfo -i input

Exemplo de saída (para o arquivo music.wav de exemplo acima):

$ wavinfo -i music.wav

riff tag        (4 bytes): "RIFF"
riff size       (4 bytes): 8061776
wave tag        (4 bytes): "WAVE"
form tag        (4 bytes): "fmt "
fmt_size        (4 bytes): 16
audio_format    (2 bytes): 1
num_channels    (2 bytes): 2
sample_rate     (4 bytes): 44100
byte_rate       (4 bytes): 176400
block_align     (2 bytes): 4
bits_per_sample (2 bytes): 16
data tag        (4 bytes): "data"
data size       (4 bytes): 8061740
bytes per sample         : 2
samples per channel      : 2015435

Ajuste de volume

O filtro de ajuste de volume permite aumentar ou diminuir o volume de áudio do arquivo, de acordo com um fator de ajuste V (0.0 ≤ V ≤ 10.0, com default em 1.0). Ele consiste basicamente em multiplicar o valor de cada amostra de áudio por V.

Forma de chamada (-l : level):

wavvol -l V -i input -o output

Exemplo de uso e saída: music-vol.wav

wavvol -l 0.1 -i music.wav -o music-vol.wav

Normalização

Este filtro faz a normalização do áudio, ou seja, o ajuste automático de volume. Para tal, é necessário encontrar o valor do maior pico no sinal de áudio (em todos os canais) e usá-lo para calcular um fator de ajuste, de modo que todas as amostras de todos os canais fiquem no intervalo de 16 bits com sinal [-32767 … +32767].

Forma de chamada:

wavnorm -i input -o output

Reversão

Este filtro produz como saída um áudio invertido, ou seja, “de trás para a frente” em relação ao áudio de entrada.

Forma de chamada:

wavrev -i input -o output

Exemplo de uso e saída: music-rev.wav

wavrev -i music.wav -o music-rev.wav
Muitos músicos se divertiram com esse efeito, como nesta música do grupo Pink Floyd!

Eco

Este filtro produz como saída um áudio com eco. O eco é controlado pelos parâmetros delay ( inteiro ≥ 0, default 1000 ms), que define o atraso do eco em milissegundos, e level (0.0 ≤ level ≤ 1.0, default 0.5), que define o nível do eco em relação ao sinal original.

O efeito de eco pode ser definido por esta equação: samplet = samplet + (level × samplet-delay)

Forma de chamada (-t : time em ms, -l : level em %):

wavecho -t delay -l level -i input -o output

Exemplo de uso e saída (ambos com t=500ms e l=50%): batida-echo.wav music-echo.wav

wavecho -t 500 -l 0.5 -i music.wav -o music-echo.wav
Experimente aplicar este filtro a um arquivo de voz, com um atraso bem pequeno (10 a 50 ms) =)

Estéreo ampliado

O filtro de estéreo ampliado permite aumentar a separação de canais em um sinal estéreo, gerando um som mais aberto. Este filtro só pode ser aplicado a sinais estéreo (com 2 canais).

Sendo R(t) e L(t) as amostras de entrada do canal direito e esquerdo em um instante t, a saída R'(t), L'(t) é calculada da seguinte forma:

diff  = R(t) - L(t)
R'(t) = R(t) + k * diff
L'(t) = L(t) - k * diff

onde diff é o sinal de diferença entre os dois canais e k é o fator de ampliação do efeito estéreo (0.0 ≤ k ≤ 10.0, default 1.0).

Forma de chamada (-l : level):

wavwide -l k -i input -o output

Exemplo de uso e saída: music-wide.wav

cat music.wav | wavvol -l 0.5 | wavwide -l 5 | wavnorm > music-wide.wav

O exemplo acima mostra o uso de vários filtros concatenados: no início o volume é reduzido para evitar a saturação do áudio, em seguida o efeito wide é aplicado e depois o áudio é normalizado e salvo no arquivo de saída. Todas as transferências de dados são feitas por stdin e stdout.


Concatenação

O filtro de concatenação recebe como entrada um ou mais arquivos de áudio e gera uma saída contendo a concatenação das entradas na sequência indicada.

Forma de chamada:

wavcat arq1.wav arq2.wav arq3.wav ... -o output
A combinação de sinais de áudio com taxas de amostragem diferentes exige um procedimento chamado reamostragem (resampling), que pode ser complexo. Por isso, restrinja seu programa a arquivos com a mesma taxa de amostragem.

Mistura

O filtro de mistura (mixagem) recebe como entrada um ou mais arquivos de áudio e gera uma saída contendo a mistura (mixagem) das entradas.

Forma de chamada:

wavmix arq1.wav arq2.wav arq3.wav ... -o output
Ao somar as amostras, cuide para não saturar a saída, o que pode gerar distorção (clipping).

Outros filtros (ideias)

Mais conteúdo sobre efeitos de áudio:


Atividade

O projeto consiste em implementar os filtros acima definidos.

Requisitos

Para simplificar a implementação e evitar erros, sugere-o o uso dos tipos inteiros de tamanho fixo (int16_t, etc), acessíveis através do arquivo inttypes.h.

Linha de comando

Essas opções podem ser usadas em qualquer combinação, ou seja:

// entrada e saída em arquivos
wavvol -i inputfile.wav  -o outputfile.wav
wavvol -o outputfile.wav -i inputfile.wav

// entrada em arquivo, saída em stdout, vice-versa ou ambos
wavvol -i inputfile.wav  > outputfile.wav
wavvol -o outputfile.wav < inputfile.wav
wavvol  < inputfile.wav  > outputfile.wav

// as opções podem estar em qualquer ordem
wavvol -l 0.3 -i inputfile.wav -o outputfile.wav
wavvol -i inputfile.wav -l 0.3 -o outputfile.wav
wavvol -o outputfile.wav -i inputfile.wav -l 0.3
Para ler e tratar mais facilmente as opções da linha de comando, sugere-se usar funções já prontas para isso, como getopt ou arg_parse (link)

Como os filtros devem tratar a entrada e saída padrão, é possível combinar filtros usando pipes UNIX. Por exemplo, podemos usar pipes para construir o efeito Reverse Echo, muito apreciado por alguns grupos de Rock:

wavrev -i input.wav | wavecho -t 500 -l 0.5 | wavrev -o output.wav

Caso a adição de eco provoque clipping, pode-se atenuar o sinal antes de processá-lo:

wavvol -l 0.5 -i input.wav | wavrev | wavecho -t 500 -l 0.5 | wavrev | wavnorm -o output.wav
Para tocar facilmente os arquivos de áudio, pode-se usar os comandos play, aplay ou paplay no terminal do Linux, ou o preview de arquivos no gerenciador de arquivos do ambiente gráfico.

Estrutura do código-fonte

O código-fonte deve ser estruturado em diversos arquivos .c e .h que contenham as funcionalidades a serem implementadas. A figura abaixo traz uma uma sugestão de estrutura para o código-fonte (as setas correspondem a includes):

Estrutura do código-fonte