67 lines
2.4 KiB
Python
67 lines
2.4 KiB
Python
|
|
import numpy as np
|
||
|
|
from mixer import normalize_rms, mix_streams, soft_limit
|
||
|
|
|
||
|
|
TARGET_DBFS = -20
|
||
|
|
|
||
|
|
|
||
|
|
class TestNormalizeRms:
|
||
|
|
def test_silence_stays_silent(self, silence):
|
||
|
|
result = normalize_rms(silence, TARGET_DBFS)
|
||
|
|
assert np.allclose(result, 0.0)
|
||
|
|
|
||
|
|
def test_quiet_tone_gets_louder(self, quiet_tone):
|
||
|
|
original_rms = np.sqrt(np.mean(quiet_tone ** 2))
|
||
|
|
result = normalize_rms(quiet_tone, TARGET_DBFS)
|
||
|
|
result_rms = np.sqrt(np.mean(result ** 2))
|
||
|
|
assert result_rms > original_rms
|
||
|
|
|
||
|
|
def test_loud_tone_gets_quieter(self, loud_tone):
|
||
|
|
original_rms = np.sqrt(np.mean(loud_tone ** 2))
|
||
|
|
result = normalize_rms(loud_tone, TARGET_DBFS)
|
||
|
|
result_rms = np.sqrt(np.mean(result ** 2))
|
||
|
|
assert result_rms < original_rms
|
||
|
|
|
||
|
|
def test_normalized_to_target(self, loud_tone):
|
||
|
|
result = normalize_rms(loud_tone, TARGET_DBFS)
|
||
|
|
result_rms = np.sqrt(np.mean(result ** 2))
|
||
|
|
result_dbfs = 20 * np.log10(result_rms + 1e-10)
|
||
|
|
assert abs(result_dbfs - TARGET_DBFS) < 1.0 # within 1 dB
|
||
|
|
|
||
|
|
|
||
|
|
class TestMixStreams:
|
||
|
|
def test_single_stream_unchanged(self, loud_tone):
|
||
|
|
result = mix_streams([loud_tone])
|
||
|
|
assert np.allclose(result, loud_tone)
|
||
|
|
|
||
|
|
def test_two_streams_summed(self, loud_tone):
|
||
|
|
result = mix_streams([loud_tone, loud_tone])
|
||
|
|
# Two identical streams summed should be louder
|
||
|
|
assert np.max(np.abs(result)) > np.max(np.abs(loud_tone))
|
||
|
|
|
||
|
|
def test_empty_list_returns_silence(self, silence):
|
||
|
|
result = mix_streams([])
|
||
|
|
assert result.shape[0] == 960
|
||
|
|
assert np.allclose(result, 0.0)
|
||
|
|
|
||
|
|
def test_different_lengths_uses_shortest(self):
|
||
|
|
short = np.ones(480, dtype=np.float32) * 0.5
|
||
|
|
long = np.ones(960, dtype=np.float32) * 0.5
|
||
|
|
result = mix_streams([short, long])
|
||
|
|
assert result.shape[0] == 480
|
||
|
|
|
||
|
|
|
||
|
|
class TestSoftLimit:
|
||
|
|
def test_quiet_signal_unchanged(self, quiet_tone):
|
||
|
|
result = soft_limit(quiet_tone)
|
||
|
|
assert np.allclose(result, quiet_tone, atol=0.001)
|
||
|
|
|
||
|
|
def test_clipping_signal_contained(self, clipping_tone):
|
||
|
|
result = soft_limit(clipping_tone)
|
||
|
|
assert np.max(np.abs(result)) <= 1.0
|
||
|
|
|
||
|
|
def test_preserves_sign(self, clipping_tone):
|
||
|
|
result = soft_limit(clipping_tone)
|
||
|
|
# Signs should match where input is non-zero
|
||
|
|
nonzero = np.abs(clipping_tone) > 0.01
|
||
|
|
assert np.all(np.sign(result[nonzero]) == np.sign(clipping_tone[nonzero]))
|