Skip to content

Commit 94700bc

Browse files
Fix IIRFilter coeff handling and add equal-loudness filter
1 parent 678dedb commit 94700bc

File tree

3 files changed

+180
-70
lines changed

3 files changed

+180
-70
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
from __future__ import annotations
2+
3+
from audio_filters.iir_filter import IIRFilter
4+
5+
6+
# Coefficients from the "Original ReplayGain specification" (Equal Loudness Filter)
7+
# - Yulewalk: 10th-order IIR
8+
# - Butterworth: 2nd-order high-pass at 150 Hz
9+
# Source tables / coefficient file:
10+
# - https://wiki.hydrogenaudio.org/index.php?title=Original_ReplayGain_specification
11+
# - https://replaygain.hydrogenaudio.org/equal_loud_coef.txt
12+
# (We embed the coefficients to avoid external dependencies and file I/O.)
13+
_REPLAYGAIN_COEFFS: dict[int, dict[str, list[float]]] = {
14+
44100: {
15+
"yule_a": [
16+
1.0,
17+
-3.47845948550071,
18+
6.36317777566148,
19+
-8.54751527471874,
20+
9.47693607801280,
21+
-8.81498681370155,
22+
6.85401540936998,
23+
-4.39470996079559,
24+
2.19611684890774,
25+
-0.75104302451432,
26+
0.13149317958808,
27+
],
28+
"yule_b": [
29+
0.05418656406430,
30+
-0.02911007808948,
31+
-0.00848709379851,
32+
-0.00851165645469,
33+
-0.00834990904936,
34+
0.02245293253339,
35+
-0.02596338512915,
36+
0.01624864962975,
37+
-0.00240879051584,
38+
0.00674613682247,
39+
-0.00187763777362,
40+
],
41+
"butter_a": [1.0, -1.96977855582618, 0.97022847566350],
42+
"butter_b": [0.98500175787242, -1.97000351574484, 0.98500175787242],
43+
},
44+
48000: {
45+
"yule_a": [
46+
1.0,
47+
-3.84664617118067,
48+
7.81501653005538,
49+
-11.34170355132042,
50+
13.05504219327545,
51+
-12.28759895145294,
52+
9.48293806319790,
53+
-5.87257861775999,
54+
2.75465861874613,
55+
-0.86984376593551,
56+
0.13919314567432,
57+
],
58+
"yule_b": [
59+
0.03857599435200,
60+
-0.02160367184185,
61+
-0.00123395316851,
62+
-0.00009291677959,
63+
-0.01655260341619,
64+
0.02161526843274,
65+
-0.02074045215285,
66+
0.00594298065125,
67+
0.00306428023191,
68+
0.00012025322027,
69+
0.00288463683916,
70+
],
71+
"butter_a": [1.0, -1.97223372919527, 0.97261396931306],
72+
"butter_b": [0.98621192462708, -1.97242384925416, 0.98621192462708],
73+
},
74+
32000: {
75+
"yule_a": [
76+
1.0,
77+
-2.37898834973084,
78+
2.84868151156327,
79+
-2.64577170229825,
80+
2.23697657451713,
81+
-1.67148153367602,
82+
1.00595954808547,
83+
-0.45953458054983,
84+
0.16378164858596,
85+
-0.05032077717131,
86+
0.02347897407020,
87+
],
88+
"yule_b": [
89+
0.15457299681924,
90+
-0.09331049056315,
91+
-0.06247880153653,
92+
0.02163541888798,
93+
-0.05588393329856,
94+
0.04781476674921,
95+
0.00222312597743,
96+
0.03174092540049,
97+
-0.01390589421898,
98+
0.00651420667831,
99+
-0.00881362733839,
100+
],
101+
"butter_a": [1.0, -1.95835380975398, 0.95920349965459],
102+
"butter_b": [0.97938932735214, -1.95877865470428, 0.97938932735214],
103+
},
104+
}
105+
106+
107+
class EqualLoudnessFilter:
108+
r"""
109+
Equal-loudness compensation filter (ReplayGain-style).
110+
111+
This is a cascade of:
112+
- 10th-order "yulewalk" IIR filter
113+
- 2nd-order Butterworth high-pass filter at 150 Hz
114+
115+
Coefficients are embedded for a few common sample rates, matching the
116+
Original ReplayGain specification. :contentReference[oaicite:1]{index=1}
117+
118+
>>> filt = EqualLoudnessFilter(44100)
119+
>>> filt.process(0.0)
120+
0.0
121+
122+
>>> EqualLoudnessFilter(12345)
123+
Traceback (most recent call last):
124+
...
125+
ValueError: Unsupported samplerate 12345. Supported samplerates: 32000, 44100, 48000
126+
"""
127+
128+
def __init__(self, samplerate: int = 44100) -> None:
129+
if samplerate not in _REPLAYGAIN_COEFFS:
130+
supported = ", ".join(str(sr) for sr in sorted(_REPLAYGAIN_COEFFS))
131+
raise ValueError(
132+
f"Unsupported samplerate {samplerate}. Supported samplerates: {supported}"
133+
)
134+
135+
coeffs = _REPLAYGAIN_COEFFS[samplerate]
136+
137+
self.yulewalk_filter = IIRFilter(10)
138+
self.yulewalk_filter.set_coefficients(coeffs["yule_a"], coeffs["yule_b"])
139+
140+
self.butterworth_filter = IIRFilter(2)
141+
self.butterworth_filter.set_coefficients(coeffs["butter_a"], coeffs["butter_b"])
142+
143+
def reset(self) -> None:
144+
"""Reset the internal filter histories to zero."""
145+
self.yulewalk_filter.input_history = [0.0] * self.yulewalk_filter.order
146+
self.yulewalk_filter.output_history = [0.0] * self.yulewalk_filter.order
147+
self.butterworth_filter.input_history = [0.0] * self.butterworth_filter.order
148+
self.butterworth_filter.output_history = [0.0] * self.butterworth_filter.order
149+
150+
def process(self, sample: float) -> float:
151+
"""
152+
Process a single sample through both filters.
153+
154+
>>> filt = EqualLoudnessFilter()
155+
>>> filt.process(0.0)
156+
0.0
157+
"""
158+
tmp = self.yulewalk_filter.process(sample)
159+
return self.butterworth_filter.process(tmp)

audio_filters/equal_loudness_filter.py.broken.txt

Lines changed: 0 additions & 61 deletions
This file was deleted.

audio_filters/iir_filter.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,27 @@ def set_coefficients(self, a_coeffs: list[float], b_coeffs: list[float]) -> None
4444
4545
This method works well with scipy's filter design functions
4646
47-
>>> # Make a 2nd-order 1000Hz butterworth lowpass filter
48-
>>> import scipy.signal
49-
>>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000,
50-
... btype='lowpass',
51-
... fs=48000)
47+
>>> # Make a 2nd-order 1000Hz butterworth lowpass filter (SciPy optional)
48+
>>> import scipy.signal # doctest: +SKIP
49+
>>> b_coeffs, a_coeffs = scipy.signal.butter(2, 1000, btype='lowpass', fs=48000) # doctest: +SKIP
50+
>>> filt = IIRFilter(2) # doctest: +SKIP
51+
>>> filt.set_coefficients(a_coeffs, b_coeffs) # doctest: +SKIP
52+
53+
>>> # a0 can be omitted (defaults to 1.0)
54+
>>> filt = IIRFilter(2)
55+
>>> filt.set_coefficients([0.5, 0.25], [0.1, 0.2, 0.3])
56+
>>> filt.a_coeffs
57+
[1.0, 0.5, 0.25]
58+
59+
>>> # b_coeffs length check should report len(b_coeffs)
5260
>>> filt = IIRFilter(2)
53-
>>> filt.set_coefficients(a_coeffs, b_coeffs)
61+
>>> filt.set_coefficients([1.0, 0.5, 0.25], [0.1, 0.2])
62+
Traceback (most recent call last):
63+
...
64+
ValueError: Expected b_coeffs to have 3 elements for 2-order filter, got 2
5465
"""
55-
if len(a_coeffs) < self.order:
66+
# Allow omitting a0 (use 1.0 as default) when only a1..ak are provided
67+
if len(a_coeffs) == self.order:
5668
a_coeffs = [1.0, *a_coeffs]
5769

5870
if len(a_coeffs) != self.order + 1:
@@ -65,7 +77,7 @@ def set_coefficients(self, a_coeffs: list[float], b_coeffs: list[float]) -> None
6577
if len(b_coeffs) != self.order + 1:
6678
msg = (
6779
f"Expected b_coeffs to have {self.order + 1} elements "
68-
f"for {self.order}-order filter, got {len(a_coeffs)}"
80+
f"for {self.order}-order filter, got {len(b_coeffs)}"
6981
)
7082
raise ValueError(msg)
7183

@@ -97,4 +109,4 @@ def process(self, sample: float) -> float:
97109
self.input_history[0] = sample
98110
self.output_history[0] = result
99111

100-
return result
112+
return result

0 commit comments

Comments
 (0)