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 )
0 commit comments