33import pyfar as pf
44from pyrato .parameters import clarity
55
6- # Input types, Output type
7- # -------------------
8- def test_accepts_signal_object ():
9- sig = pf .signals .impulse (
6+
7+ def test_clarity_accepts_signal_object_and_returns_correct_type ():
8+ impulse_signal = pf .signals .impulse (
109 n_samples = 128 , delay = 0 , amplitude = 1 , sampling_rate = 44100
1110 )
12- result = clarity (sig , early_time_limit = 80 )
11+ result = clarity (impulse_signal , early_time_limit = 1 )
1312
1413 assert isinstance (result , (float , np .ndarray ))
15- assert result .shape == sig .cshape
16-
17- def test_rejects_non_signal_input ():
18- with pytest .raises (AttributeError ):
19- clarity (np .array ([1 ,2 ,3 ]))
14+ assert result .shape == impulse_signal .cshape
2015
2116
17+ def test_clarity_rejects_non_signal_input ():
18+ with pytest .raises (AttributeError ):
19+ clarity (np .array ([1 , 2 , 3 ]))
2220
23- # Multichannel shape
24- # -------------------
25- def test_multichannel_shape ():
26- brir = pf .signals .files .binaural_room_impulse_response (diffuse_field_compensation = False , sampling_rate = 48000 )
2721
22+ def test_clarity_preserves_multichannel_shape ():
23+ brir = pf .signals .files .binaural_room_impulse_response (
24+ diffuse_field_compensation = False ,
25+ sampling_rate = 48000
26+ )
2827 output = clarity (brir , early_time_limit = 80 )
2928
3029 assert brir .cshape == output .shape
3130
3231
33-
34-
35- # Edge cases
36- # -------------------
37- def test_empty_signal_returns_nan ():
38- sig = pf .Signal (np .zeros (16 ), 44100 )
39- result = clarity (sig , early_time_limit = 80 )
32+ def test_clarity_returns_nan_for_zero_signal ():
33+ silent_signal = pf .Signal (np .zeros (4096 ), 44100 )
34+ result = clarity (silent_signal )
4035 assert np .isnan (result ) or result == - np .inf
4136
4237
43- def test_unusual_time_limit_warns ():
44- sig = pf .signals .impulse ( n_samples = 128 , delay = 0 , amplitude = 1 , sampling_rate = 44100 )
38+ def test_clarity_warns_for_unusually_short_time_limit ():
39+ impulse_signal = pf .signals .impulse (
40+ n_samples = 128 , delay = 0 , amplitude = 1 , sampling_rate = 44100
41+ )
4542 with pytest .warns (UserWarning ):
46- clarity (sig , early_time_limit = 0.05 )
47-
48-
49-
50-
51- # Energie- vs. Amplitudensignal
52- # -------------------
53- # def test_energy_signal_vs_amplitude_signal():
54- # # gleicher Inhalt, einmal als "energy" markiert
55- # sig_amp = pf.signals.impulse( n_samples=128, delay=0, amplitude=1, sampling_rate=44100)
56- # sig_energy = pf.Signal(sig_amp.time**2, 44100)
57-
58- # # label "energy" automatically assigned?
59- # result_amp = clarity(sig_amp, early_time_limit=80)
60- # result_energy = clarity(sig_energy, early_time_limit=80)
43+ clarity (impulse_signal , early_time_limit = 0.05 )
6144
62- # # same values
63- # np.testing.assert_allclose(result_amp, result_energy)
6445
65-
66- # Reference cases
67- # -------------------
68- def test_known_reference_case ():
69- # Artificial signal: 2 Samples early = 2 energy, 2 Samples late = 2 energy
70- data = np .array ([1 , 1 , 1 , 1 ] + [0 ]* 124 )
71- sig = pf .Signal (data , sampling_rate = 1000 )
72- result = clarity (sig , early_time_limit = 2 ) # 2ms limit -> even early/late division
73- # Early = Late => log10(1) = 0 dB
74- assert np .isclose (result , 0.0 , atol = 1e-6 )
75-
76- def load_c80_from_rew (file_path ):
77- c80_values = []
78-
79- with open (file_path , "r" ) as f :
80- for line in f :
81- line = line .strip ()
82- if not line or not line [0 ].isdigit (): # nur Zeilen, die mit Zahl anfangen
83- continue
84- parts = [p .strip () for p in line .split ("," )]
85- if len (parts ) < 16 :
86- continue
87- try :
88- c80 = float (parts [15 ])
89- c80_values .append (c80 )
90- except ValueError :
91- continue
92-
93- return np .array (c80_values , dtype = np .float32 )
94-
95-
96- # test with reference impulse response C80
97- def test_reference_impulse_response_filtered ():
98- rir = pf .signals .files .room_impulse_response (sampling_rate = 48000 )
99-
100- # filter into octave bands
101- rir_octave_filtered = pf .dsp .filter .fractional_octave_bands (rir , num_fractions = 1 )
102- # rir_third_octave_filtered = pf.dsp.filter.fractional_octave_bands(rir, num_fractions=3)
103-
104- c80_rir_octave_bands = clarity (rir_octave_filtered , early_time_limit = 80 )
105-
106- # tolerance?
107- REW_c80_rir_octave_band = load_c80_from_rew ("tests/test_data/example_rir48kHz_REWdata.txt" )
46+ def test_clarity_calculates_known_reference_value ():
47+ # 2 samples early energy = 2, 2 samples late energy = 2 → ratio = 1 → 0 dB
48+ signal_data = np .concatenate (([1 , 1 , 1 , 1 ], np .zeros (124 )))
49+ test_signal = pf .Signal (signal_data , sampling_rate = 1000 )
10850
51+ result = clarity (test_signal , early_time_limit = 2 )
10952
110- assert np .allclose (REW_c80_rir_octave_band , c80_rir_octave_bands , atol = 0.01 )
111-
53+ assert np .isclose (result , 0.0 , atol = 1e-6 )
11254
11355
56+ def test_clarity_matches_analytical_geometric_decay_solution ():
57+ sampling_rate = 1000
58+ decay_factor = 0.9
59+ total_samples = 200
60+ early_cutoff = 80 # ms
11461
115- # test with reference impulse response C50
62+ time_axis = np .arange (total_samples )
63+ decaying_signal = pf .Signal (
64+ decay_factor ** time_axis ,
65+ sampling_rate = sampling_rate
66+ )
11667
68+ squared_factor = decay_factor ** 2
69+ early_energy = (1 - squared_factor ** early_cutoff ) / (1 - squared_factor )
70+ late_energy = (squared_factor ** early_cutoff - squared_factor ** total_samples ) / (1 - squared_factor )
71+ expected_db = 10 * np .log10 (early_energy / late_energy )
11772
118- # test with reference impulse response C1000
119- # what happens if early_time limit is longer than IR?
120- # what happens if early_time_limit is out of logical bounds?
73+ result = clarity (decaying_signal , early_time_limit = early_cutoff )
12174
75+ assert np .isclose (result , expected_db , atol = 1e-6 )
0 commit comments