Ok, fiquei cansado de usar os filtros do Audacity, então é hora de construir nosso próprio filtro passa-baixas. Até onde sei, há três métodos básicos de desenhar um filtro digital, como o filtro passa-baixas que precisamos para a modulação AM. O mais comum é o filtro FIR. Outro método é o filtro IIR.
E o terceiro método é a Transformada Discreta de Fourier (DFT). Neste artigo, vou utilizar o método mais fácil, que é justamente DFT. Outros artigos abordam FIR e IIR.
DFT é um conceito matemático; é a versão "digital" da transformada de Fourier. A implementação mais comumente utilizada chama-se FFT (transformada rápida de Fourier). A a quase totalidade das bibliotecas matemáticas oferece FFT. A biblioteca Python-Numeric não é exceção.
DFT converte um sinal no domínio do tempo, por exemplo um arquivo WAV, para uma representação no domínio da freqüência. O analisador de espectro (figura mais acima) é quase a mesma coisa, embora o DFT costume ser calculado com muito mais resolução que as meras 18 barras (por canal) da figura.
Ou seja, o DFT permite dissecar um áudio em graves, médios, agudos, até por nota musical — bem como inúmeras outras aplicações.
DFT não é um processamento unidirecional. Ele pode ser invertido. Dada uma representação no domínio da freqüência, podemos executar a transformada inversa (IDFT) e obter a onda no domínio do tempo correspondente. Então, nós podemos pegar um áudio qualquer, transformar com DFT, manipular o espectro (aumentar graves, diminuir agudos, etc.) e converter de volta para áudio WAV.
Em nosso caso, queremos construir filtros passa-baixas e passa-altas, então só precisamos eliminar do espectro as freqüências que não desejamos. A rigor, um corte brusco não é a melhor forma de configurar um filtro, pois causa fortes distorções de fase nas freqüências próximas aos cortes, mas é bom o suficiente para demonstração.
Converter de/para o domínio das freqüências parece trabalho demais. De fato, os métodos FIR e IIR permitem fazer essa filtragem diretamente no domínio do tempo, e normalmente seriam as escolhas certas em aplicações práticas. Porém eles não são tão fáceis de entender, nem tão versáteis.
Neste post, utilizarei a voz de uma cantora de que gosto muito: Núbia Lafayette. Outra razão de ter escolhido este áudio é porque eu possuo o CD, então ninguém pode reclamar que estou fazendo pirataria. Vamos ouvir o original:
Agora, a versão filtrada (filtro passa-banda 1000 a 4000Hz), que soa como aqueles radinhos de pilha:
E o código que fez a mágica:
#!/usr/bin/env python LOWPASS = 4000 # Hz HIGHPASS = 1000 # Hz SAMPLE_RATE = 44100 # Hz import wave, struct, math from numpy import fft FFT_LENGTH = 2048 # The higher the better, but this seems enough OVERLAP = 512 FFT_SAMPLE = FFT_LENGTH - OVERLAP # Nyquist rate = bandwidth available for a given sample rate NYQUIST_RATE = SAMPLE_RATE / 2.0 # Convert frequencies from Hz to our digital sampling units LOWPASS /= (NYQUIST_RATE / (FFT_LENGTH / 2.0)) HIGHPASS /= (NYQUIST_RATE / (FFT_LENGTH / 2.0)) zeros = [ 0 for x in range(0, OVERLAP) ] # Builds filter mask. Note that this filter is BAD, a # good filter must have a smooth curve, respecting a # dB/octave attenuation ramp! mask = [] for f in range(0, FFT_LENGTH / 2 + 1): rampdown = 1.0 if f > LOWPASS: rampdown = 0.0 elif f < HIGHPASS: rampdown = 0.0 mask.append(rampdown) def bound(sample): # takes care of "burnt" samples, due some mistake in filter design if sample > 1.0: print "!", sample = 1.0 elif sample < -1.0: print "!", sample = -1.0 return sample original = wave.open("NubiaCantaDalva.wav", "r") filtered = wave.open("NubiaFiltered.wav", "w") filtered.setnchannels(1) filtered.setsampwidth(2) filtered.setframerate(SAMPLE_RATE) n = original.getnframes() original = struct.unpack('%dh' % n, original.readframes(n)) # scale from 16-bit signed WAV to float original = [s / 32768.0 for s in original] saved_td = zeros for pos in range(0, len(original), FFT_SAMPLE): time_sample = original[pos : pos + FFT_LENGTH] # convert frame to frequency domain representation frequency_domain = fft.fft(time_sample, FFT_LENGTH) l = len(frequency_domain) # mask positive frequencies (f[0] is DC component) for f in range(0, l/2+1): frequency_domain[f] *= mask[f] # mask negative frequencies # http://stackoverflow.com/questions/933088/clipping-fft-matrix for f in range(l-1, l/2, -1): cf = l - f frequency_domain[f] *= mask[cf] # convert frame back to time domain time_domain = fft.ifft(frequency_domain) # modified overlap-add logic: previously saved samples # prevail in the beginning, and they are ramped down # in favor of this frame's samples, to avoid 'clicks' # in either end of frame. for i in range(0, OVERLAP): time_domain[i] *= (i + 0.0) / OVERLAP time_domain[i] += saved_td[i] * (1.0 - (i + 0.00) / OVERLAP) # reserve last samples for the next frame saved_td = time_domain[FFT_SAMPLE:] # do not write reserved samples right now time_domain = time_domain[:FFT_SAMPLE] # scale back into WAV 16-bit and write time_domain = [ bound(sample) * 32767 for sample in time_domain ] filtered.writeframes(struct.pack('%dh' % len(time_domain), *time_domain))
Existe uma pegadinha neste código que merece ser mencionada. Toda filtragem, mesmo que executada no domínio da freqüência, causa distorção de fase. A filtragem DFT trabalha o áudio em blocos fixos, mas você não pode simplesmente reproduzir um bloco após o outro. A descontinuidade é audível como estalos, bem desagradáveis. (Se você não acredita, mude a variável OVERLAP para zero e veja o que acontece.)
Ambas as soluções (overlap-save e overlap-add) que encontrei em livros especializados não pareceram resolver o problema. Modifiquei o método overlap-add, o que aparentemente resolveu o caso. Minha solução faz fade-in do bloco "n+1" ao mesmo tempo que faz fade-out do bloco "n", juntando os dois de forma suave. Talvez haja solução melhor, mas esta gerou áudio de boa qualidade e não encontrei outra forma na literatura.
Outra coisa que deve ser lembrada por qualquer um que vá brincar com FFT, é que a representação do domínio da freqüência é linear. Em geral, estamos acostumados a uma representação logarítmica da mesma, porque corresponde às notas musicais. Isto significa que a resolução de freqüência de uma matriz gerada pelo FFT é (musicalmente) muito mais precisa nas oitavas baixas.
O FFT gera dois "lobos" de freqüência: positivas e negativas. Isto parece muito estranho, já que não existem freqüências negativas no mundo real. Porém o FFT tem esse cacoete matemático. Em nosso filtro, nós tínhamos duas escolhas: simplesmente zerar as freqüências negativas (o que causaria uma redução de volume do áudio final em 50%), ou então filtrá-las da mesma forma que as freqüências positivas (que é o que fazemos).
A matriz gerada pelo FFT consiste de números complexos. Isto soa assustador, mas tem um propósito válido: um número complexo pode representar amplitude e fase de forma compacta. Por exemplo, se encontramos uma amostra 1+0j em 3000Hz, isto significa que o áudio original tem um componente de 3000Hz, amplitude=1 e em fase com cos(x). Se a amostra fosse 0.707+0.707j, isto significaria que o componente ainda tem amplitude=1 (módulo do número complexo) porém a fase é de 45 graus em relação a cos(x).
Algumas aplicações, como o bonito analisador de espectro da figura inicial, não precisam da fase do sinal. Neste caso, podemos utilizar DCT - Transformada Discreta de Cosseno, cujo resultado é uma matriz de números reais. A forma de onda original ainda pode ser obtida pela transformada inversa (IDCT) mas neste caso o sinal reconstituído terá fase diferente do original.