1+ """
2+ Tests for the Equal Loudness Filter implementation.
3+
4+ This module contains comprehensive tests for the EqualLoudnessFilter class,
5+ including functionality tests, edge cases, and numerical validation.
6+ """
7+
8+ import math
9+ from unittest .mock import patch
10+
11+ import pytest
12+
13+ from audio_filters .equal_loudness_filter import EqualLoudnessFilter , _yulewalk_approximation
14+
15+
16+ class TestYulewalkApproximation :
17+ """Test cases for the Yule-Walker approximation function."""
18+
19+ def test_basic_functionality (self ):
20+ """Test basic functionality of Yule-Walker approximation."""
21+ import numpy as np
22+
23+ frequencies = np .array ([0.0 , 0.25 , 0.5 , 0.75 , 1.0 ])
24+ gains = np .array ([1.0 , 0.8 , 0.6 , 0.4 , 0.2 ])
25+
26+ a_coeffs , b_coeffs = _yulewalk_approximation (4 , frequencies , gains )
27+
28+ # Check that coefficients are numpy arrays
29+ assert isinstance (a_coeffs , np .ndarray )
30+ assert isinstance (b_coeffs , np .ndarray )
31+
32+ # Check correct length
33+ assert len (a_coeffs ) == 5 # order + 1
34+ assert len (b_coeffs ) == 5 # order + 1
35+
36+ # Check normalization (first a coefficient should be 1.0)
37+ assert a_coeffs [0 ] == 1.0
38+
39+ def test_edge_case_empty_data (self ):
40+ """Test behavior with minimal data points."""
41+ import numpy as np
42+
43+ frequencies = np .array ([0.0 , 1.0 ])
44+ gains = np .array ([1.0 , 0.5 ])
45+
46+ a_coeffs , b_coeffs = _yulewalk_approximation (2 , frequencies , gains )
47+
48+ # Should still return valid coefficients
49+ assert len (a_coeffs ) == 3
50+ assert len (b_coeffs ) == 3
51+ assert a_coeffs [0 ] == 1.0
52+
53+ def test_zero_gains_handling (self ):
54+ """Test handling of zero gains (should not cause divide by zero)."""
55+ import numpy as np
56+
57+ frequencies = np .array ([0.0 , 0.5 , 1.0 ])
58+ gains = np .array ([0.0 , 0.0 , 0.0 ]) # All zeros
59+
60+ a_coeffs , b_coeffs = _yulewalk_approximation (2 , frequencies , gains )
61+
62+ # Should handle gracefully without crashing
63+ assert len (a_coeffs ) == 3
64+ assert len (b_coeffs ) == 3
65+ assert a_coeffs [0 ] == 1.0
66+
67+
68+ class TestEqualLoudnessFilter :
69+ """Test cases for the EqualLoudnessFilter class."""
70+
71+ def test_initialization_default (self ):
72+ """Test default initialization."""
73+ filt = EqualLoudnessFilter ()
74+
75+ assert filt .samplerate == 44100
76+ assert filt .yulewalk_filter .order == 10
77+ assert hasattr (filt , 'butterworth_filter' )
78+
79+ def test_initialization_custom_samplerate (self ):
80+ """Test initialization with custom sample rate."""
81+ samplerate = 48000
82+ filt = EqualLoudnessFilter (samplerate )
83+
84+ assert filt .samplerate == samplerate
85+
86+ def test_initialization_invalid_samplerate (self ):
87+ """Test that invalid sample rates raise ValueError."""
88+ with pytest .raises (ValueError , match = "Sample rate must be positive" ):
89+ EqualLoudnessFilter (0 )
90+
91+ with pytest .raises (ValueError , match = "Sample rate must be positive" ):
92+ EqualLoudnessFilter (- 1000 )
93+
94+ def test_process_silence (self ):
95+ """Test processing silence (zero input)."""
96+ filt = EqualLoudnessFilter ()
97+ result = filt .process (0.0 )
98+
99+ assert isinstance (result , float )
100+ assert result == 0.0
101+
102+ def test_process_various_inputs (self ):
103+ """Test processing various input types and values."""
104+ filt = EqualLoudnessFilter ()
105+
106+ test_inputs = [0.0 , 0.1 , - 0.1 , 0.5 , - 0.5 , 1.0 , - 1.0 ]
107+
108+ for input_val in test_inputs :
109+ result = filt .process (input_val )
110+ assert isinstance (result , float )
111+ assert math .isfinite (result ) # Result should be finite
112+
113+ def test_process_integer_input (self ):
114+ """Test that integer inputs are handled correctly."""
115+ filt = EqualLoudnessFilter ()
116+
117+ result = filt .process (1 ) # Integer input
118+ assert isinstance (result , float )
119+ assert math .isfinite (result )
120+
121+ def test_process_consistency (self ):
122+ """Test that same input produces same output (deterministic)."""
123+ filt1 = EqualLoudnessFilter ()
124+ filt2 = EqualLoudnessFilter ()
125+
126+ test_value = 0.5
127+ result1 = filt1 .process (test_value )
128+ result2 = filt2 .process (test_value )
129+
130+ # Should produce same result for same input on fresh filters
131+ assert result1 == result2
132+
133+ def test_filter_memory (self ):
134+ """Test that filter maintains internal state (memory)."""
135+ filt = EqualLoudnessFilter ()
136+
137+ # Process the same input multiple times
138+ results = []
139+ for _ in range (3 ):
140+ results .append (filt .process (1.0 ))
141+
142+ # Results should potentially differ due to internal state
143+ # (This tests that the filter has memory)
144+ assert len (results ) == 3
145+
146+ def test_reset_functionality (self ):
147+ """Test the reset method."""
148+ filt = EqualLoudnessFilter ()
149+
150+ # Process some samples to build up internal state
151+ for _ in range (5 ):
152+ filt .process (0.5 )
153+
154+ # Reset the filter
155+ filt .reset ()
156+
157+ # Internal history should be cleared
158+ assert all (val == 0.0 for val in filt .yulewalk_filter .input_history )
159+ assert all (val == 0.0 for val in filt .yulewalk_filter .output_history )
160+
161+ def test_get_filter_info (self ):
162+ """Test the filter info method."""
163+ samplerate = 48000
164+ filt = EqualLoudnessFilter (samplerate )
165+
166+ info = filt .get_filter_info ()
167+
168+ # Check that info contains expected keys
169+ expected_keys = {
170+ 'samplerate' , 'yulewalk_order' , 'yulewalk_a_coeffs' ,
171+ 'yulewalk_b_coeffs' , 'butterworth_order'
172+ }
173+ assert set (info .keys ()) == expected_keys
174+
175+ # Check some values
176+ assert info ['samplerate' ] == samplerate
177+ assert info ['yulewalk_order' ] == 10
178+ assert isinstance (info ['yulewalk_a_coeffs' ], list )
179+ assert isinstance (info ['yulewalk_b_coeffs' ], list )
180+
181+ def test_different_samplerates (self ):
182+ """Test filter behavior with different sample rates."""
183+ samplerates = [22050 , 44100 , 48000 , 96000 ]
184+
185+ for sr in samplerates :
186+ filt = EqualLoudnessFilter (sr )
187+ result = filt .process (0.5 )
188+ assert isinstance (result , float )
189+ assert math .isfinite (result )
190+
191+ @patch ('audio_filters.equal_loudness_filter.data' )
192+ def test_missing_data_handling (self , mock_data ):
193+ """Test handling when JSON data is malformed or missing."""
194+ # Mock corrupted data
195+ mock_data .__getitem__ .side_effect = KeyError ("Missing key" )
196+
197+ with pytest .raises (KeyError ):
198+ EqualLoudnessFilter ()
199+
200+ def test_docstring_examples (self ):
201+ """Test examples from the class docstring."""
202+ # Test basic instantiation
203+ filt = EqualLoudnessFilter (48000 )
204+ processed_sample = filt .process (0.5 )
205+ assert isinstance (processed_sample , float )
206+
207+ # Test silence processing
208+ filt = EqualLoudnessFilter ()
209+ result = filt .process (0.0 )
210+ assert result == 0.0
211+
212+ def test_extreme_values (self ):
213+ """Test filter behavior with extreme input values."""
214+ filt = EqualLoudnessFilter ()
215+
216+ extreme_values = [1e6 , - 1e6 , 1e-6 , - 1e-6 ]
217+
218+ for val in extreme_values :
219+ result = filt .process (val )
220+ # Result should be finite (no overflow/underflow issues)
221+ assert math .isfinite (result )
222+
223+ def test_high_frequency_samplerates (self ):
224+ """Test with very high sample rates."""
225+ high_samplerates = [192000 , 384000 ]
226+
227+ for sr in high_samplerates :
228+ filt = EqualLoudnessFilter (sr )
229+ result = filt .process (0.1 )
230+ assert isinstance (result , float )
231+ assert math .isfinite (result )
232+
233+
234+ class TestFilterStability :
235+ """Test cases for filter stability and numerical properties."""
236+
237+ def test_stability_impulse_response (self ):
238+ """Test that impulse response decays (filter is stable)."""
239+ filt = EqualLoudnessFilter ()
240+
241+ # Apply impulse (1.0 followed by zeros)
242+ responses = []
243+ responses .append (filt .process (1.0 )) # Impulse
244+
245+ # Follow with zeros and record responses
246+ for _ in range (20 ):
247+ responses .append (filt .process (0.0 ))
248+
249+ # Response should generally decay towards zero for stable filter
250+ # (allowing for some numerical variation)
251+ assert len (responses ) == 21
252+ assert all (math .isfinite (r ) for r in responses )
253+
254+ def test_no_dc_buildup (self ):
255+ """Test that constant input doesn't cause DC buildup."""
256+ filt = EqualLoudnessFilter ()
257+
258+ # Apply constant input for many samples
259+ constant_input = 0.1
260+ responses = []
261+ for _ in range (100 ):
262+ responses .append (filt .process (constant_input ))
263+
264+ # Check that response doesn't grow without bound
265+ assert all (math .isfinite (r ) for r in responses )
266+ assert max (abs (r ) for r in responses ) < 1000 # Reasonable bound
267+
268+
269+ if __name__ == "__main__" :
270+ # Simple manual test runner if pytest is not available
271+ print ("Running basic tests for EqualLoudnessFilter..." )
272+
273+ # Test basic functionality
274+ try :
275+ filt = EqualLoudnessFilter ()
276+ result = filt .process (0.0 )
277+ assert result == 0.0
278+ print ("✓ Silence test passed" )
279+
280+ result = filt .process (0.5 )
281+ assert isinstance (result , float )
282+ print ("✓ Basic processing test passed" )
283+
284+ filt .reset ()
285+ print ("✓ Reset test passed" )
286+
287+ info = filt .get_filter_info ()
288+ assert isinstance (info , dict )
289+ print ("✓ Filter info test passed" )
290+
291+ print ("\n All basic tests passed! 🎉" )
292+
293+ except Exception as e :
294+ print (f"❌ Test failed: { e } " )
0 commit comments