1
1
"""
2
2
Equal-loudness filter implementation for audio processing.
3
3
4
- This module implements an equal-loudness filter which compensates for the human ear's
4
+ This module implements an equal-loudness filter which compensates for the human ear's
5
5
non-linear response to sound using cascaded IIR filters.
6
6
"""
7
7
@@ -23,44 +23,44 @@ def _yulewalk_approximation(
23
23
) -> tuple [np .ndarray , np .ndarray ]:
24
24
"""
25
25
Simplified Yule-Walker approximation for filter design.
26
-
26
+
27
27
This is a basic implementation that approximates the yulewalker functionality
28
28
using numpy for creating filter coefficients from frequency response data.
29
-
29
+
30
30
Args:
31
31
order: Filter order
32
32
frequencies: Normalized frequencies (0 to 1)
33
33
gains: Desired gains at those frequencies
34
-
34
+
35
35
Returns:
36
36
Tuple of (a_coeffs, b_coeffs) for the IIR filter
37
37
"""
38
38
# Simple approach: create coefficients that approximate the desired response
39
39
# This is a simplified version - in practice, yulewalker uses more sophisticated methods
40
-
40
+
41
41
# Create a basic filter response approximation
42
42
# Using a simple polynomial fit approach
43
43
try :
44
44
# Fit polynomial to log-magnitude response
45
45
log_gains = np .log10 (gains + 1e-10 ) # Avoid log(0)
46
46
coeffs = np .polyfit (frequencies , log_gains , min (order , len (frequencies ) - 1 ))
47
-
47
+
48
48
# Convert polynomial coefficients to filter coefficients
49
49
a_coeffs = np .zeros (order + 1 )
50
50
b_coeffs = np .zeros (order + 1 )
51
-
51
+
52
52
a_coeffs [0 ] = 1.0 # Normalized
53
-
53
+
54
54
# Simple mapping from polynomial to filter coefficients
55
55
for i in range (min (len (coeffs ), order )):
56
56
b_coeffs [i ] = coeffs [- (i + 1 )] * 0.1 # Scale factor for stability
57
-
57
+
58
58
# Ensure some basic coefficients are set
59
59
if b_coeffs [0 ] == 0 :
60
60
b_coeffs [0 ] = 0.1
61
-
61
+
62
62
return a_coeffs , b_coeffs
63
-
63
+
64
64
except (np .linalg .LinAlgError , ValueError ):
65
65
# Fallback to simple pass-through filter
66
66
a_coeffs = np .zeros (order + 1 )
@@ -74,27 +74,27 @@ class EqualLoudnessFilter:
74
74
"""
75
75
An equal-loudness filter which compensates for the human ear's non-linear response
76
76
to sound.
77
-
77
+
78
78
This filter corrects the frequency response by cascading a Yule-Walker approximation
79
79
filter and a Butterworth high-pass filter.
80
-
81
- The filter is designed for use with sample rates of 44.1kHz and above. If you're
80
+
81
+ The filter is designed for use with sample rates of 44.1kHz and above. If you're
82
82
using a lower sample rate, use with caution.
83
-
83
+
84
84
The equal-loudness contours are based on the Robinson-Dadson curves (1956), which
85
85
describe how the human ear perceives different frequencies at various loudness levels.
86
-
86
+
87
87
References:
88
88
- Robinson, D. W., & Dadson, R. S. (1956). A re-determination of the equal-
89
89
loudness relations for pure tones. British Journal of Applied Physics, 7(5), 166.
90
90
- Original MATLAB implementation by David Robinson, 2001
91
-
91
+
92
92
Examples:
93
93
>>> filt = EqualLoudnessFilter(48000)
94
94
>>> processed_sample = filt.process(0.5)
95
95
>>> isinstance(processed_sample, float)
96
96
True
97
-
97
+
98
98
>>> # Process silence
99
99
>>> filt = EqualLoudnessFilter()
100
100
>>> filt.process(0.0)
@@ -104,17 +104,17 @@ class EqualLoudnessFilter:
104
104
def __init__ (self , samplerate : int = 44100 ) -> None :
105
105
"""
106
106
Initialize the equal-loudness filter.
107
-
107
+
108
108
Args:
109
109
samplerate: Sample rate in Hz (default: 44100)
110
-
110
+
111
111
Raises:
112
112
ValueError: If samplerate is not positive
113
113
"""
114
114
if samplerate <= 0 :
115
115
msg = "Sample rate must be positive"
116
116
raise ValueError (msg )
117
-
117
+
118
118
self .samplerate = samplerate
119
119
self .yulewalk_filter = IIRFilter (10 )
120
120
self .butterworth_filter = make_highpass (150 , samplerate )
@@ -138,17 +138,17 @@ def __init__(self, samplerate: int = 44100) -> None:
138
138
def process (self , sample : Union [float , int ]) -> float :
139
139
"""
140
140
Process a single sample through both filters.
141
-
141
+
142
142
The sample is first processed through the Yule-Walker approximation filter
143
143
to apply the equal-loudness curve correction, then through a high-pass
144
144
Butterworth filter to remove low-frequency artifacts.
145
-
145
+
146
146
Args:
147
147
sample: Input audio sample (should be normalized to [-1, 1] range)
148
-
148
+
149
149
Returns:
150
150
Processed audio sample as float
151
-
151
+
152
152
Examples:
153
153
>>> filt = EqualLoudnessFilter()
154
154
>>> filt.process(0.0)
@@ -160,17 +160,17 @@ def process(self, sample: Union[float, int]) -> float:
160
160
"""
161
161
# Convert to float for processing
162
162
sample_float = float (sample )
163
-
163
+
164
164
# Apply Yule-Walker approximation filter first
165
165
tmp = self .yulewalk_filter .process (sample_float )
166
-
166
+
167
167
# Then apply Butterworth high-pass filter
168
168
return self .butterworth_filter .process (tmp )
169
169
170
170
def reset (self ) -> None :
171
171
"""
172
172
Reset the filter's internal state (clear history).
173
-
173
+
174
174
This is useful when starting to process a new audio stream
175
175
to avoid artifacts from previous processing.
176
176
"""
@@ -181,7 +181,7 @@ def reset(self) -> None:
181
181
def get_filter_info (self ) -> dict [str , Union [int , float , list [float ]]]:
182
182
"""
183
183
Get information about the filter configuration.
184
-
184
+
185
185
Returns:
186
186
Dictionary containing filter parameters and coefficients
187
187
"""
@@ -197,17 +197,17 @@ def get_filter_info(self) -> dict[str, Union[int, float, list[float]]]:
197
197
if __name__ == "__main__" :
198
198
# Demonstration of the filter
199
199
import doctest
200
-
200
+
201
201
doctest .testmod ()
202
-
202
+
203
203
# Create a simple test
204
204
filter_instance = EqualLoudnessFilter (44100 )
205
205
test_samples = [0.0 , 0.1 , 0.5 , - 0.3 , 1.0 , - 1.0 ]
206
-
206
+
207
207
print ("Equal-Loudness Filter Demo:" )
208
208
print ("Sample Rate: 44100 Hz" )
209
209
print ("Test samples and their filtered outputs:" )
210
-
210
+
211
211
for sample in test_samples :
212
212
filtered = filter_instance .process (sample )
213
- print (f"Input: { sample :6.1f} → Output: { filtered :8.6f} " )
213
+ print (f"Input: { sample :6.1f} → Output: { filtered :8.6f} " )
0 commit comments