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.

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:

  • Taxa de amostragem: é o número de amostras do sinal analógico feitas por segundo. Quanto maior a taxa de amostragem, melhor é a representação digital do som, sobretudo nas frequências mais elevadas (agudos). Por outro lado, taxas de amostragem elevadas implicam em arquivos maiores. Valores típicos de taxas de amostragem são 48 kHz em DVDs e 44,1 KHz em CDs de música (exemplos).
  • Resolução: é o número de bits usado para representar cada amostra. Quanto maior a resolução, mais precisa é a representação de cada amostra e menor a distorção do sinal. CDs de música usam 16 bits de resolução, enquanto sistemas de telefonia costumam usar 8 bits (exemplos).

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.

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.

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.

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

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

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

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!

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

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.


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.

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

  • extrair um canal de um áudio stereo
  • juntar dois áudios mono em um áudio stereo
  • recortar um áudio de t1 a t2
  • aplicar um atraso t a um áudio (t pode ser negativo, para adiantar o áudio)
  • smoothing: trocar cada amostra pela média entre ela e suas vizinhas
  • efeito phaser, vibrato e outros
  • compressor
  • filtro de ruído: reduzir o volume de todas as amostras com nível abaixo de um certo valor (dado em % do volume máximo).
  • … (outras ideias são bem-vindas)

Mais conteúdo sobre efeitos de áudio:


O projeto consiste em implementar os filtros acima definidos.

  • Cada filtro é um comando separado. Por exemplo, o filtro de volume deve ser implementado em um arquivo wavvol.c que gera um executável wavvol, e assim por diante.
  • Os filtros devem aceitar como entrada sons no formato WAV PCM 16 bits com sinal e devem gerar como saída sons nesse mesmo formato.
  • As rotinas comuns (leitura/escrita de arquivos, tratamento da linha de comando, etc) devem ser implementadas em arquivos separados, cujos cabeçalhos são incluídos nos arquivos de implementação dos filtros.
  • Sempre que possível, as informações do som necessárias às funções devem ser transferidas como parâmetros (por valor ou por referência, dependendo da situação). Minimizar o uso de variáveis globais.
  • Use alocação dinâmica de memória para os sons, para poder processar arquivos grandes. Só aloque a memória para as amostras após encontrar o número de amostras no cabeçalho.
  • Construir um Makefile para o projeto:
    • Ao menos os alvos all (default), clean e purge.
    • CFLAGS = -Wall
    • Compilar e ligar separadamente (gerar arquivos .o intermediários)
  • O que deve ser entregue ao professor:
    • arquivos .c e .h
    • arquivo Makefile
    • não enviar os sons de teste
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.
  • A opção -i indica o nome do arquivo de entrada; se não for informado, deve-se usar a entrada padrão (stdin).
  • A opção -o indica o nome do arquivo de saída; se não for informado, deve-se usar a saída padrão (stdout).
  • Todas as mensagens de erro devem ser enviadas para a saída de erro (stderr).

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.

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

  • c/processamento_de_audio.txt
  • Última modificação: 2023/08/01 16:23
  • por 127.0.0.1