Så bygger du ett bandspärrfilter med separata kontroller för bredd och centrumfrekvens
Bygg ett digitalt bandspärrfilter med två parametrar: centrumfrekvens i hertz och spärrbandsbredd i hertz. Beräkna om filterkoefficienterna varje gång någon av dem ändras. Om ditt filterbibliotek använder Q i stället för bandbredd ska du beräkna Q = center / width vid varje uppdatering, så att den begärda bredden bevaras när centrumfrekvensen flyttas.
Den här handledningen bygger en fungerande ljudprototyp i Python med enbart standardbiblioteket. Den skapar en testsignal, bearbetar den, verifierar spärrbandsbredden och kan valfritt exportera WAV-filer för avlyssning. Kontrollerna är kommandoradsparametrar; samma koefficientfunktion kan senare användas bakom två rattar i ett ljudprogram.
Vad ska de två kontrollerna faktiskt ändra?
Centrumkontrollen ställer in frekvensen för maximal dämpning. Breddkontrollen ställer in avståndet mellan de två gränserna vid halva effekten, vanligen kallade −3 dB-punkterna. En notch är en smalbandig form av ett bandspärrfilter; SciPys officiella dokumentation definierar dess kvalitetsfaktor som centrumfrekvensen dividerad med den här bandbredden. SciPy: iirnotch
Välj enhet för bredden innan du utformar kontrollerna. En bredd uttryckt i hertz bevarar ett absolut frekvensspann. En bredd uttryckt i oktaver beskriver ett frekvensförhållande. W3C:s Audio EQ Cookbook stöder uttryckligen bandbredd i oktaver för notchfilter och tar hänsyn till frekvensförvrängning i sina digitala koefficientformler. Det här är olika kontrollbeteenden, så ange enheten tydligt. W3C: Audio EQ Cookbook
I den här lösningen använder vi hertz. Anta att centrumfrekvensen är 1 000 Hz och bredden är 200 Hz: motsvarande Q är 5. Om centrumfrekvensen flyttas till 2 000 Hz medan bredden på 200 Hz behålls måste Q ändras till 10. Om Q i stället hålls vid 5 blir bandbredden 400 Hz. De här illustrativa beräkningarna förklarar varför en kontroll med fast Q inte uppfyller uppgiften vi har valt.
Vilken filterstruktur ger separata kontroller?
Använd ett andra ordningens IIR-filter (Infinite Impulse Response), även kallat biquad. Det har tre täljarkoefficienter och tre nämnarkoefficienter. När den första nämnarkoefficienten har normaliserats till ett blir sampelns ekvation:
[ y[n]=b_0x[n]+b_1x[n-1]+b_2x[n-2]-a_1y[n-1]-a_2y[n-2]. ]
Det här är biquad-sambandet som dokumenteras i Audio EQ Cookbook. W3C: Audio EQ Cookbook
För en samplingsfrekvens \(F_s\), centrumfrekvens \(f_c\) och bredd \(B\), beräknar du:
\[ \omega_c=2\pi f_c/F_s,\qquad \beta=\tan(\pi B/F_s),\qquad g=\frac{1}{1+\beta}. \]
Använd sedan:
\[ b=g[1,-2\cos(\omega_c),1], \]
\[ a=[1,-2g\cos(\omega_c),2g-1]. \]
Det här är notchkoefficientekvationerna som används i SciPys implementation, omskrivna så att bandbredd i hertz är direkt indata. SciPys kälkod för filterdesign
Båda kontrollerna kan ändra flera koefficienter. De är oberoende i fråga om den resulterande responsen: när bredden ändras ligger dämpningsfrekvensen kvar, och när centrumfrekvensen ändras ligger den angivna bandbredden kvar. Att koppla varje ratt till bara en koefficient garanterar inte det beteendet.
Hur bygger du en fungerande ljudprototyp?
Spara följande fullständiga program som band_reject.py. Det kräver Python 3.8 eller senare och inga ytterligare paket. Samplingsfrekvensen är 48 000 Hz, och den genererade insignalen innehåller tre toner på 300, 1 000 och 5 000 Hz. Varje ton har amplituden 0,2.
Programmet beräknar filterresponsen direkt, hittar båda gränserna vid halva effekten med bisektion och bearbetar två sekunder genererat ljud. WAV-exporten använder Pythons dokumenterade gränssnitt wave för att skriva monofiler med 16-bitars PCM. Om utdatafilerna redan finns uppstår ett fel i stället för att de skrivs över. Python: wave — läsa och skriva WAV-filer
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()
Kör python band_reject.py --center 1000 --width 200 för att kontrollera responsen. Lägg till --write för att skapa input-demo.wav och filtered-demo.wav. Importera filerna till separata spår i ett ljudredigeringsprogram, justera så att de börjar samtidigt och jämför dem med oförändrad uppspelningsvolym.
Jag körde hela programmet utan filexport. Kontrollerna av bandbredd, dämpning vid centrumfrekvensen, utdatalängd och ändliga utvärden passerade. WAV-skrivningen testades inte i den körningen. Den utskrivna responsen var ungefär −0,02 dB vid 300 Hz och −0,01 dB vid 5 000 Hz; värdet vid centrum nådde visningsgränsen på −240 dB. Den gränsen är en numerisk rapporteringsgräns, inte ett uppmätt dämpningsdjup i ljudfilen.
Hur verifierar du att kontrollerna är oberoende?
Testa tre inställningar: en baslinje, ett ändrat centrum med oförändrad bredd och en ändrad bredd med oförändrat centrum. Då isoleras varje kontroll. Följande värden beräknades genom att koefficient- och gränssökningsrutinerna kördes vid 48 kHz; de beskriver den matematiska responsen, inte en inspelningsmätning.
| Centruminställning | Breddinställning | Nedre gräns vid halva effekten | Övre gräns vid halva effekten | Beräknat spann |
|---|---|---|---|---|
| 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 |
Observera att gränserna inte är exakt centrum ± bredd/2. Här anger ”centrum” dämpningsnollan, medan ”bredd” anger avståndet mellan skärningspunkterna vid halva effekten. Den beräknade responsen visar att deras aritmetiska medelvärde inte nödvändigtvis sammanfaller med nollfrekvensen. Bygg inte en gränsvisning genom att helt enkelt addera och subtrahera halva bredden.
För en extra numerisk kontroll i en miljö med SciPy installerat kan du utvärdera de returnerade koefficienterna med scipy.signal.freqz. Ange fs och explicita frekvenser, inklusive den exakta centrumfrekvensen. Dokumentationen förklarar att en explicit frekvensarray styr var responsen utvärderas; ett glest standardrutnät kan missa en smal nolla. SciPy: freqz
Hur väljer du centrum och bredd för ett ljud?
Använd ett kort, repeterbart avsnitt av ljudet du redigerar. Identifiera först frekvensområdet du vill minska och ställ sedan in centrumfrekvensen där. Börja med en smal bredd och öka den tills dämpningen täcker det avsedda området. Jämför med förbikoppling efter varje ändring och håll uppspelningsvolymen konstant så att jämförelsen förblir användbar.
Använd den här beslutshjälpen för att välja nästa justering:
| Vad jämförelsen visar | Nästa justering |
|---|---|
| Den oönskade tonen finns kvar, men närliggande innehåll förändras | Kontrollera centrumfrekvensen igen innan du ökar bredden |
| Centrumfrekvensen är rätt, men det oönskade området sträcker sig längre | Öka bredden gradvis |
| För mycket av klangen runt omkring försvinner | Minska bredden och jämför igen |
| Flera åtskilda toner behöver minskas | Prova separata notchfiltersektioner vid de frekvenserna |
| Ett brett, jämnt dämpat område behövs | Överväg filtertypen och ordningen på nytt |
Det här är praktiska förslag på justeringar, inte mätningar från en viss inspelning. Exemplet med tre toner är avsiktligt enkelt: det låter dig kontrollera att processorn fungerar som den ska innan du bedömer ett mer komplext ljud.
Ett enskilt notchfilter ger inte heller någon separat justerbar djupkontroll. Cookbooken innehåller separata koefficientfamiljer för notchfilter och peaking-EQ-filter. Om uppgiften kräver en måttlig, justerbar sänkning i stället för en dämpningsnolla är peaking-EQ ett annat lämpligt alternativ att undersöka. W3C: Audio EQ Cookbook
Vad ändras när du kopplar in två rattar för realtidsstyrning?
Behåll samma parameterkontrakt: center_hz och width_hz skickas tillsammans till koefficientfunktionen. Uppdatera hela uppsättningen koefficienter när någon av parametrarna ändras och beräkna om dem när samplingsfrekvensen ändras. I den här prototypen måste båda kontrollerna vara strikt större än noll och mindre än halva samplingsfrekvensen; ogiltiga inställningar ger ett undantag.
Den medföljande funktionen process() hanterar en hel monobuffert och börjar med noll historik vid varje anrop. En version för strömmande ljud måste behålla fördröjningshistoriken mellan blocken. SciPys lfilter dokumenterar det initiala fördröjningstillståndet och det returnerade sluttillståndet för detta ändamål. Behåll separat historik för varje ljudkanal. SciPy: lfilter
Behandla rörelse i kontrollerna som ett extra implementeringssteg. Det här exemplet verifierar fasta inställningar; det hanterar inte kontinuerlig automatisering. En praktisk metod för övergången är att tillfälligt köra det gamla och det nya fasta filtret parallellt, låta det nya filtret bygga upp sin historik och tona över mellan deras utdata. Bedöm övergången med det faktiska materialet och den avsedda kontrollhastigheten.
Om flera filter kopplas i serie ska du behålla varje filter som en separat sektion av andra ordningen i stället för att expandera kedjan till ett polynom av högre ordning. SciPy rekommenderar sektioner av andra ordningen för de flesta filteruppgifter eftersom de ger färre numeriska problem. SciPy: lfilter
Vilka fel bör du kontrollera först?
Om notchfiltret verkar ligga vid fel frekvens ska du kontrollera att samplingsfrekvensen som användes vid koefficientberäkningen stämmer med ljudets faktiska samplingsfrekvens. Om bredden ökar när du flyttar centrumfrekvensen ska du kontrollera om din omslagsfunktion håller Q konstant. Om responsvisningen ser grund ut ska du utvärdera den exakta centrumfrekvensen innan du ändrar konstruktionen.
Om upprepade buffertar ger diskontinuiteter ska du granska tillståndshanteringen: demonstrationen nollställer historiken varje gång process() anropas. Om WAV-exporten ger ett intervallfel ska du sänka insignalamplituden och köra igen; programmet vägrar avsiktligt att klippa sampel utanför intervallet utan att säga till. Du måste byta namn på eller ta bort befintliga demonstrationsfiler innan du exporterar ett nytt par.
Lösningen är klar när centrumtestet placerar nollan där den begärts, de två gränspassagerna ligger det önskade avståndet från varandra och det bearbetade ljudet fungerar för den avsedda redigeringen. Behåll de kontrollerna tillsammans med de två reglagen när du går från den här offlineprototypen till ett interaktivt ljudverktyg.
