Skip to content

Commit 00a6701

Browse files
Simple audio filters (#5230)
* Add IIR Filter and Butterworth design functions Signed-off-by: Martmists <[email protected]> * naming conventions and missing type hints Signed-off-by: Martmists <[email protected]> * Link wikipedia in IIRFilter Signed-off-by: Martmists <[email protected]> * Add doctests and None return types Signed-off-by: Martmists <[email protected]> * More doctests Signed-off-by: Martmists <[email protected]> * Requested changes Signed-off-by: Martmists <[email protected]> * run pre-commit Signed-off-by: Martmists <[email protected]> * Make mypy stop complaining about ints vs floats Signed-off-by: Martmists <[email protected]> * Use slower listcomp to make it more readable Signed-off-by: Martmists <[email protected]> * Make doctests happy Signed-off-by: Martmists <[email protected]> * Remove scipy Signed-off-by: Martmists <[email protected]> * Test coefficients from bw filters Signed-off-by: Martmists <[email protected]> * Protocol test Co-authored-by: Christian Clauss <[email protected]> * Make requested change Signed-off-by: Martmists <[email protected]> * Types Signed-off-by: Martmists <[email protected]> * Apply suggestions from code review * Apply suggestions from code review * Update butterworth_filter.py Co-authored-by: Christian Clauss <[email protected]>
1 parent bd9464e commit 00a6701

File tree

4 files changed

+403
-0
lines changed

4 files changed

+403
-0
lines changed

audio_filters/__init__.py

Whitespace-only changes.

audio_filters/butterworth_filter.py

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
from math import cos, sin, sqrt, tau
2+
3+
from audio_filters.iir_filter import IIRFilter
4+
5+
"""
6+
Create 2nd-order IIR filters with Butterworth design.
7+
8+
Code based on https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html
9+
Alternatively you can use scipy.signal.butter, which should yield the same results.
10+
"""
11+
12+
13+
def make_lowpass(
14+
frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)
15+
) -> IIRFilter:
16+
"""
17+
Creates a low-pass filter
18+
19+
>>> filter = make_lowpass(1000, 48000)
20+
>>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE
21+
[1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.004277569313094809,
22+
0.008555138626189618, 0.004277569313094809]
23+
"""
24+
w0 = tau * frequency / samplerate
25+
_sin = sin(w0)
26+
_cos = cos(w0)
27+
alpha = _sin / (2 * q_factor)
28+
29+
b0 = (1 - _cos) / 2
30+
b1 = 1 - _cos
31+
32+
a0 = 1 + alpha
33+
a1 = -2 * _cos
34+
a2 = 1 - alpha
35+
36+
filt = IIRFilter(2)
37+
filt.set_coefficients([a0, a1, a2], [b0, b1, b0])
38+
return filt
39+
40+
41+
def make_highpass(
42+
frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)
43+
) -> IIRFilter:
44+
"""
45+
Creates a high-pass filter
46+
47+
>>> filter = make_highpass(1000, 48000)
48+
>>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE
49+
[1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.9957224306869052,
50+
-1.9914448613738105, 0.9957224306869052]
51+
"""
52+
w0 = tau * frequency / samplerate
53+
_sin = sin(w0)
54+
_cos = cos(w0)
55+
alpha = _sin / (2 * q_factor)
56+
57+
b0 = (1 + _cos) / 2
58+
b1 = -1 - _cos
59+
60+
a0 = 1 + alpha
61+
a1 = -2 * _cos
62+
a2 = 1 - alpha
63+
64+
filt = IIRFilter(2)
65+
filt.set_coefficients([a0, a1, a2], [b0, b1, b0])
66+
return filt
67+
68+
69+
def make_bandpass(
70+
frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)
71+
) -> IIRFilter:
72+
"""
73+
Creates a band-pass filter
74+
75+
>>> filter = make_bandpass(1000, 48000)
76+
>>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE
77+
[1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.06526309611002579,
78+
0, -0.06526309611002579]
79+
"""
80+
w0 = tau * frequency / samplerate
81+
_sin = sin(w0)
82+
_cos = cos(w0)
83+
alpha = _sin / (2 * q_factor)
84+
85+
b0 = _sin / 2
86+
b1 = 0
87+
b2 = -b0
88+
89+
a0 = 1 + alpha
90+
a1 = -2 * _cos
91+
a2 = 1 - alpha
92+
93+
filt = IIRFilter(2)
94+
filt.set_coefficients([a0, a1, a2], [b0, b1, b2])
95+
return filt
96+
97+
98+
def make_allpass(
99+
frequency: int, samplerate: int, q_factor: float = 1 / sqrt(2)
100+
) -> IIRFilter:
101+
"""
102+
Creates an all-pass filter
103+
104+
>>> filter = make_allpass(1000, 48000)
105+
>>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE
106+
[1.0922959556412573, -1.9828897227476208, 0.9077040443587427, 0.9077040443587427,
107+
-1.9828897227476208, 1.0922959556412573]
108+
"""
109+
w0 = tau * frequency / samplerate
110+
_sin = sin(w0)
111+
_cos = cos(w0)
112+
alpha = _sin / (2 * q_factor)
113+
114+
b0 = 1 - alpha
115+
b1 = -2 * _cos
116+
b2 = 1 + alpha
117+
118+
filt = IIRFilter(2)
119+
filt.set_coefficients([b2, b1, b0], [b0, b1, b2])
120+
return filt
121+
122+
123+
def make_peak(
124+
frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)
125+
) -> IIRFilter:
126+
"""
127+
Creates a peak filter
128+
129+
>>> filter = make_peak(1000, 48000, 6)
130+
>>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE
131+
[1.0653405327119334, -1.9828897227476208, 0.9346594672880666, 1.1303715025601122,
132+
-1.9828897227476208, 0.8696284974398878]
133+
"""
134+
w0 = tau * frequency / samplerate
135+
_sin = sin(w0)
136+
_cos = cos(w0)
137+
alpha = _sin / (2 * q_factor)
138+
big_a = 10 ** (gain_db / 40)
139+
140+
b0 = 1 + alpha * big_a
141+
b1 = -2 * _cos
142+
b2 = 1 - alpha * big_a
143+
a0 = 1 + alpha / big_a
144+
a1 = -2 * _cos
145+
a2 = 1 - alpha / big_a
146+
147+
filt = IIRFilter(2)
148+
filt.set_coefficients([a0, a1, a2], [b0, b1, b2])
149+
return filt
150+
151+
152+
def make_lowshelf(
153+
frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)
154+
) -> IIRFilter:
155+
"""
156+
Creates a low-shelf filter
157+
158+
>>> filter = make_lowshelf(1000, 48000, 6)
159+
>>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE
160+
[3.0409336710888786, -5.608870992220748, 2.602157875636628, 3.139954022810743,
161+
-5.591841778072785, 2.5201667380627257]
162+
"""
163+
w0 = tau * frequency / samplerate
164+
_sin = sin(w0)
165+
_cos = cos(w0)
166+
alpha = _sin / (2 * q_factor)
167+
big_a = 10 ** (gain_db / 40)
168+
pmc = (big_a + 1) - (big_a - 1) * _cos
169+
ppmc = (big_a + 1) + (big_a - 1) * _cos
170+
mpc = (big_a - 1) - (big_a + 1) * _cos
171+
pmpc = (big_a - 1) + (big_a + 1) * _cos
172+
aa2 = 2 * sqrt(big_a) * alpha
173+
174+
b0 = big_a * (pmc + aa2)
175+
b1 = 2 * big_a * mpc
176+
b2 = big_a * (pmc - aa2)
177+
a0 = ppmc + aa2
178+
a1 = -2 * pmpc
179+
a2 = ppmc - aa2
180+
181+
filt = IIRFilter(2)
182+
filt.set_coefficients([a0, a1, a2], [b0, b1, b2])
183+
return filt
184+
185+
186+
def make_highshelf(
187+
frequency: int, samplerate: int, gain_db: float, q_factor: float = 1 / sqrt(2)
188+
) -> IIRFilter:
189+
"""
190+
Creates a high-shelf filter
191+
192+
>>> filter = make_highshelf(1000, 48000, 6)
193+
>>> filter.a_coeffs + filter.b_coeffs # doctest: +NORMALIZE_WHITESPACE
194+
[2.2229172136088806, -3.9587208137297303, 1.7841414181566304, 4.295432981120543,
195+
-7.922740859457287, 3.6756456963725253]
196+
"""
197+
w0 = tau * frequency / samplerate
198+
_sin = sin(w0)
199+
_cos = cos(w0)
200+
alpha = _sin / (2 * q_factor)
201+
big_a = 10 ** (gain_db / 40)
202+
pmc = (big_a + 1) - (big_a - 1) * _cos
203+
ppmc = (big_a + 1) + (big_a - 1) * _cos
204+
mpc = (big_a - 1) - (big_a + 1) * _cos
205+
pmpc = (big_a - 1) + (big_a + 1) * _cos
206+
aa2 = 2 * sqrt(big_a) * alpha
207+
208+
b0 = big_a * (ppmc + aa2)
209+
b1 = -2 * big_a * pmpc
210+
b2 = big_a * (ppmc - aa2)
211+
a0 = pmc + aa2
212+
a1 = 2 * mpc
213+
a2 = pmc - aa2
214+
215+
filt = IIRFilter(2)
216+
filt.set_coefficients([a0, a1, a2], [b0, b1, b2])
217+
return filt

audio_filters/iir_filter.py

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from __future__ import annotations
2+
3+
4+
class IIRFilter:
5+
r"""
6+
N-Order IIR filter
7+
Assumes working with float samples normalized on [-1, 1]
8+
9+
---
10+
11+
Implementation details:
12+
Based on the 2nd-order function from
13+
https://en.wikipedia.org/wiki/Digital_biquad_filter,
14+
this generalized N-order function was made.
15+
16+
Using the following transfer function
17+
H(z)=\frac{b_{0}+b_{1}z^{-1}+b_{2}z^{-2}+...+b_{k}z^{-k}}{a_{0}+a_{1}z^{-1}+a_{2}z^{-2}+...+a_{k}z^{-k}}
18+
we can rewrite this to
19+
y[n]={\frac{1}{a_{0}}}\left(\left(b_{0}x[n]+b_{1}x[n-1]+b_{2}x[n-2]+...+b_{k}x[n-k]\right)-\left(a_{1}y[n-1]+a_{2}y[n-2]+...+a_{k}y[n-k]\right)\right)
20+
"""
21+
22+
def __init__(self, order: int) -> None:
23+
self.order = order
24+
25+
# a_{0} ... a_{k}
26+
self.a_coeffs = [1.0] + [0.0] * order
27+
# b_{0} ... b_{k}
28+
self.b_coeffs = [1.0] + [0.0] * order
29+
30+
# x[n-1] ... x[n-k]
31+
self.input_history = [0.0] * self.order
32+
# y[n-1] ... y[n-k]
33+
self.output_history = [0.0] * self.order
34+
35+
def set_coefficients(self, a_coeffs: list[float], b_coeffs: list[float]) -> None:
36+
"""
37+
Set the coefficients for the IIR filter. These should both be of size order + 1.
38+
a_0 may be left out, and it will use 1.0 as default value.
39+
40+
This method works well with scipy's filter design functions
41+
>>> # Make a 2nd-order 1000Hz butterworth lowpass filter
42+
>>> import scipy.signal
43+
>>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000,
44+
... btype='lowpass',
45+
... fs=48000)
46+
>>> filt = IIRFilter(2)
47+
>>> filt.set_coefficients(a_coeffs, b_coeffs)
48+
"""
49+
if len(a_coeffs) < self.order:
50+
a_coeffs = [1.0] + a_coeffs
51+
52+
if len(a_coeffs) != self.order + 1:
53+
raise ValueError(
54+
f"Expected a_coeffs to have {self.order + 1} elements for {self.order}"
55+
f"-order filter, got {len(a_coeffs)}"
56+
)
57+
58+
if len(b_coeffs) != self.order + 1:
59+
raise ValueError(
60+
f"Expected b_coeffs to have {self.order + 1} elements for {self.order}"
61+
f"-order filter, got {len(a_coeffs)}"
62+
)
63+
64+
self.a_coeffs = a_coeffs
65+
self.b_coeffs = b_coeffs
66+
67+
def process(self, sample: float) -> float:
68+
"""
69+
Calculate y[n]
70+
71+
>>> filt = IIRFilter(2)
72+
>>> filt.process(0)
73+
0.0
74+
"""
75+
result = 0.0
76+
77+
# Start at index 1 and do index 0 at the end.
78+
for i in range(1, self.order + 1):
79+
result += (
80+
self.b_coeffs[i] * self.input_history[i - 1]
81+
- self.a_coeffs[i] * self.output_history[i - 1]
82+
)
83+
84+
result = (result + self.b_coeffs[0] * sample) / self.a_coeffs[0]
85+
86+
self.input_history[1:] = self.input_history[:-1]
87+
self.output_history[1:] = self.output_history[:-1]
88+
89+
self.input_history[0] = sample
90+
self.output_history[0] = result
91+
92+
return result

0 commit comments

Comments
 (0)