Como criar um filtro rejeita-faixa com controles separados de largura e frequência central
Crie um filtro digital rejeita-faixa com dois parâmetros: frequência central em hertz e largura de rejeição em hertz. Recalcule os coeficientes do filtro sempre que qualquer um deles mudar. Se a biblioteca do filtro aceitar Q em vez da largura de banda, calcule Q = center / width a cada atualização para que mover a frequência central preserve a largura solicitada.
Este tutorial cria um protótipo de áudio funcional em Python usando apenas a biblioteca padrão. Ele gera um sinal de teste, processa-o, verifica a largura de rejeição e, opcionalmente, exporta arquivos WAV para audição. Os controles são parâmetros de linha de comando; a mesma função de coeficientes poderá depois ficar por trás de dois botões em um aplicativo de áudio.
O que os dois controles devem alterar, exatamente?
O controle de frequência central define a frequência de rejeição máxima. O controle de largura define a distância entre os dois limites de meia potência, convencionalmente chamados de pontos de −3 dB. Um notch é uma forma de banda estreita de um filtro rejeita-faixa; a documentação oficial do SciPy define seu fator de qualidade como a frequência central dividida por essa largura de banda. SciPy: iirnotch
Escolha a unidade da largura antes de projetar os controles. Uma largura expressa em hertz preserva um intervalo absoluto de frequência. Uma largura expressa em oitavas descreve uma razão entre frequências. O Audio EQ Cookbook do W3C oferece explicitamente suporte à largura de banda em oitavas para filtros notch e leva em conta a distorção de frequência nas fórmulas dos coeficientes digitais. Esses são comportamentos distintos dos controles, então identifique claramente a unidade escolhida. W3C: Audio EQ Cookbook
Para esta implementação, use hertz. Suponha que a frequência central seja 1.000 Hz e a largura seja 200 Hz: o Q correspondente é 5. Ao mover a frequência central para 2.000 Hz e manter a largura em 200 Hz, o Q precisa passar para 10. Manter Q em 5 produziria, em vez disso, uma largura de banda de 400 Hz. Esses cálculos ilustrativos explicam por que um controle de Q fixo não atende ao objetivo escolhido.
Qual estrutura de filtro permite controles separados?
Use um filtro de resposta ao impulso infinita de segunda ordem, também chamado de biquad. Ele tem três coeficientes no numerador e três no denominador. Depois de normalizar o primeiro coeficiente do denominador para um, sua equação de amostras é:
[ y[n]=b_0x[n]+b_1x[n-1]+b_2x[n-2]-a_1y[n-1]-a_2y[n-2]. ]
Essa é a relação do biquad documentada no Audio EQ Cookbook. W3C: Audio EQ Cookbook
Para uma taxa de amostragem \(F_s\), uma frequência central \(f_c\) e uma largura \(B\), calcule:
[ \omega_c=2\pi f_c/F_s,\qquad \beta=\tan(\pi B/F_s),\qquad g=\frac{1}{1+\beta}. ]
Em seguida, use:
[ b=g[1,-2\cos(\omega_c),1], ]
[ a=[1,-2g\cos(\omega_c),2g-1]. ]
Essas são as equações dos coeficientes de notch usadas na implementação do SciPy, reescritas para que a largura em hertz seja a entrada direta. Código-fonte do projeto de filtros do SciPy
Os dois controles podem alterar vários coeficientes. Sua independência diz respeito à resposta resultante: ao mudar a largura, a frequência de rejeição permanece fixa; ao mudar a frequência central, a largura de banda especificada permanece fixa. Atribuir cada botão a apenas um coeficiente não garantiria esse comportamento.
Como criar um protótipo de áudio funcional?
Salve o programa completo a seguir como band_reject.py. Ele requer Python 3.8 ou posterior e nenhum pacote adicional. A taxa de amostragem é 48.000 Hz, e a entrada gerada contém três tons de 300, 1.000 e 5.000 Hz. Cada tom tem amplitude de 0,2.
O programa avalia diretamente a resposta do filtro, localiza os dois limites de meia potência por bisseção e processa dois segundos de áudio gerado. A exportação WAV usa a interface documentada wave do Python para gravar arquivos mono PCM de 16 bits. Se os arquivos de saída já existirem, o programa gera um erro em vez de sobrescrevê-los. Python: wave — Ler e gravar arquivos WAV
import argparse
import cmath
import math
import struct
import wave
def coefficients(center, width, fs):
if not all(math.isfinite(v) for v in (center, width, fs)):
raise ValueError('Parameters must be finite')
if not (fs > 0 and 0 < center < fs / 2 and 0 < width < fs / 2):
raise ValueError('Require fs > 0 and center, width inside (0, fs/2)')
beta = math.tan(math.pi * width / fs)
g = 1 / (1 + beta)
c = math.cos(2 * math.pi * center / fs)
return (g, -2 * g * c, g), (1.0, -2 * g * c, 2 * g - 1)
def magnitude(f, b, a, fs):
z = cmath.exp(-2j * math.pi * f / fs)
return abs((b[0] + b[1]*z + b[2]*z*z) /
(a[0] + a[1]*z + a[2]*z*z))
def edge(lo, hi, b, a, fs):
target = 1 / math.sqrt(2)
for _ in range(60):
mid = (lo + hi) / 2
if ((magnitude(lo, b, a, fs) - target) *
(magnitude(mid, b, a, fs) - target)) > 0:
lo = mid
else:
hi = mid
return (lo + hi) / 2
def process(samples, b, a):
x1 = x2 = y1 = y2 = 0.0
result = []
for x in samples:
y = b[0]*x + b[1]*x1 + b[2]*x2 - a[1]*y1 - a[2]*y2
result.append(y)
x2, x1, y2, y1 = x1, x, y1, y
return result
def write_wav(path, samples, fs):
if any(not math.isfinite(x) or abs(x) > 1 for x in samples):
raise ValueError('Output exceeds PCM range; lower the input level')
data = b''.join(struct.pack('<h', round(x * 32767)) for x in samples)
with open(path, 'xb') as stream:
with wave.open(stream, 'wb') as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(fs)
wav.writeframes(data)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--center', type=float, default=1000)
parser.add_argument('--width', type=float, default=200)
parser.add_argument('--write', action='store_true')
args = parser.parse_args()
fs = 48000
b, a = coefficients(args.center, args.width, fs)
low = edge(0, args.center, b, a, fs)
high = edge(args.center, fs / 2, b, a, fs)
print(f'Edges: {low:.3f}, {high:.3f} Hz; width: {high-low:.3f} Hz')
assert math.isclose(high-low, args.width, abs_tol=1e-5)
assert magnitude(args.center, b, a, fs) < 1e-7
tones = (300, 1000, 5000)
for f in tones:
db = 20 * math.log10(max(magnitude(f, b, a, fs), 1e-12))
print(f'{f} Hz: {db:.2f} dB')
samples = [sum(0.2 * math.sin(2*math.pi*f*n/fs) for f in tones)
for n in range(2*fs)]
filtered = process(samples, b, a)
assert len(filtered) == len(samples)
assert all(math.isfinite(y) for y in filtered)
if args.write:
write_wav('input-demo.wav', samples, fs)
write_wav('filtered-demo.wav', filtered, fs)
if __name__ == '__main__':
main()
Execute python band_reject.py --center 1000 --width 200 para verificar a resposta. Adicione --write para criar input-demo.wav e filtered-demo.wav. Importe esses arquivos em um editor de áudio, em faixas separadas, alinhe o início e compare-os sem alterar o ganho de reprodução.
Executei o programa completo sem exportar arquivos. As asserções de largura de banda, rejeição na frequência central, comprimento da saída e valores finitos foram aprovadas. A gravação WAV não foi testada nessa execução. A resposta impressa foi de aproximadamente −0,02 dB em 300 Hz e −0,01 dB em 5.000 Hz; o valor na frequência central atingiu o limite de exibição de −240 dB. Esse limite é uma limitação numérica de exibição, não uma medida da profundidade de rejeição no arquivo de áudio.
Como verificar se os controles são independentes?
Teste três configurações: uma de referência, outra com a frequência central alterada e a largura mantida, e uma terceira com a largura alterada e a frequência central mantida. Isso isola cada controle. Os valores a seguir foram calculados executando as rotinas de coeficientes e busca dos limites a 48 kHz; eles descrevem a resposta matemática, não uma medição de gravação.
| Configuração da frequência central | Configuração da largura | Limite inferior de meia potência | Limite superior de meia potência | Intervalo calculado |
|---|---|---|---|---|
| 1.000 Hz | 200 Hz | 904,959 Hz | 1.104,959 Hz | 200,000 Hz |
| 2.000 Hz | 200 Hz | 1.902,441 Hz | 2.102,441 Hz | 200,000 Hz |
| 1.000 Hz | 400 Hz | 819,692 Hz | 1.219,692 Hz | 400,000 Hz |
Observe que os limites não são exatamente frequência central ± largura/2. Aqui, “frequência central” identifica o ponto nulo de rejeição, enquanto “largura” identifica a distância entre os cruzamentos de meia potência. A resposta calculada demonstra que seu ponto médio aritmético não precisa coincidir com a frequência do ponto nulo. Não crie uma exibição dos limites simplesmente somando e subtraindo metade da largura.
Para uma verificação numérica adicional em um ambiente com SciPy, avalie os coeficientes retornados com scipy.signal.freqz. Informe fs e as frequências explícitas, incluindo a frequência central exata. A documentação explica que um vetor explícito de frequências controla os pontos nos quais a resposta é avaliada; uma grade padrão grosseira pode não detectar um ponto nulo estreito. SciPy: freqz
Como escolher a frequência central e a largura para um som?
Use um trecho curto e repetível do som que está editando. Primeiro identifique a região de frequência que deseja reduzir e, em seguida, defina a frequência central nessa região. Comece com uma largura estreita e aumente-a até que a redução cubra a região pretendida. Compare com o bypass após cada alteração, mantendo o ganho de reprodução constante para que a comparação continue útil.
Use este guia de decisão para escolher o próximo ajuste:
| O que a comparação revela | Próximo ajuste |
|---|---|
| O tom indesejado permanece, mas o conteúdo próximo muda | Verifique novamente a frequência central antes de aumentar a largura |
| A frequência central está certa, mas a região indesejada se estende além dela | Aumente a largura gradualmente |
| O timbre ao redor desaparece em excesso | Reduza a largura e compare novamente |
| Vários tons separados precisam ser reduzidos | Avalie seções notch separadas nessas frequências |
| É necessária uma região rejeitada ampla e plana | Reconsidere o formato e a ordem do filtro |
Estas são sugestões práticas de ajuste, não medições de uma gravação específica. O exemplo de três tons é deliberadamente simples: ele permite confirmar que o processador se comporta corretamente antes de fazer avaliações sobre um som mais complexo.
Um único notch também não oferece um controle de profundidade ajustável de forma independente. O cookbook apresenta famílias distintas de coeficientes para filtros notch e de equalização peaking. Se a tarefa exigir um corte moderado e ajustável, em vez de um ponto nulo de rejeição, vale investigar também um equalizador peaking. W3C: Audio EQ Cookbook
O que muda ao conectar dois botões em tempo real?
Mantenha o mesmo contrato de parâmetros: center_hz e width_hz são fornecidos juntos à função de coeficientes. Atualize o conjunto completo de coeficientes sempre que qualquer um dos parâmetros mudar e recalcule-o quando a taxa de amostragem do processamento mudar. Neste protótipo, ambos os controles devem permanecer estritamente acima de zero e abaixo da metade da taxa de amostragem; configurações inválidas geram uma exceção.
A função fornecida process() processa um buffer mono completo e começa com o histórico zerado a cada chamada. Uma versão para fluxo contínuo deve preservar o histórico de atrasos entre blocos. A documentação lfilter do SciPy descreve o estado de atraso inicial e o estado final retornado para esse fim. Mantenha históricos separados para canais de áudio distintos. SciPy: lfilter
Trate o movimento dos controles como uma etapa adicional de implementação. Este exemplo verifica configurações fixas; ele não implementa automação contínua. Uma estratégia prática para a transição é executar brevemente os filtros antigo e novo em paralelo, permitir que o filtro novo estabeleça seu histórico e então fazer uma transição cruzada entre suas saídas. Avalie a transição com o material real e a velocidade de controle pretendida.
Para vários filtros em série, mantenha cada um como uma seção separada de segunda ordem em vez de expandir a cascata em um único polinômio de ordem alta. O SciPy recomenda seções de segunda ordem para a maioria das tarefas de filtragem, pois elas apresentam menos problemas numéricos. SciPy: lfilter
Quais condições de falha devem ser verificadas primeiro?
Se o notch aparecer na frequência errada, verifique se a taxa de amostragem usada no cálculo dos coeficientes corresponde à taxa de amostragem real do áudio. Se a largura aumentar ao mover a frequência central, verifique se o código que envolve o filtro mantém Q constante. Se a resposta parecer rasa no visor, avalie a frequência central exata antes de alterar o projeto.
Se buffers repetidos produzirem descontinuidades, verifique o tratamento do estado: a demonstração reinicia o histórico sempre que process() é chamada. Se a exportação WAV gerar um erro de intervalo, reduza a amplitude da entrada e execute novamente; o programa se recusa intencionalmente a limitar silenciosamente amostras fora do intervalo. É necessário renomear ou remover os arquivos de demonstração existentes antes de exportar outro par.
A implementação está concluída quando o teste de frequência central posiciona o ponto nulo onde foi solicitado, os dois cruzamentos dos limites permanecem à distância solicitada e o som processado atende à edição pretendida. Mantenha essas verificações junto aos dois controles ao passar deste protótipo offline para uma ferramenta de áudio interativa.
