import tempfile import unittest from pathlib import Path import numpy as np import soundfile as sf from lib.mixer import mix_vocals_and_accompaniment def _rms(audio: np.ndarray) -> float: return float(np.sqrt(np.mean(np.square(audio)) + 1e-12)) class MixStabilityTests(unittest.TestCase): def test_default_mix_does_not_duck_accompaniment_when_vocal_enters(self): sr = 48000 duration = 3.0 t = np.arange(int(sr * duration), dtype=np.float32) / sr accompaniment = 0.07 * np.sin(2.0 * np.pi * 220.0 * t) vocals = np.zeros_like(accompaniment) active = (t >= 1.10) & (t < 2.00) vocals[active] = 0.045 * np.sin(2.0 * np.pi * 440.0 * t[active]) with tempfile.TemporaryDirectory() as tmp_dir: tmp = Path(tmp_dir) vocals_path = tmp / "vocals.wav" accompaniment_path = tmp / "accompaniment.wav" output_path = tmp / "mix.wav" sf.write(vocals_path, vocals, sr) sf.write(accompaniment_path, accompaniment, sr) mix_vocals_and_accompaniment( str(vocals_path), str(accompaniment_path), str(output_path), target_sr=sr, ) mixed, out_sr = sf.read(output_path) self.assertEqual(out_sr, sr) if mixed.ndim > 1: mixed = mixed.mean(axis=1) residual = mixed.astype(np.float32) - vocals.astype(np.float32) before = residual[int(0.40 * sr): int(0.90 * sr)] during = residual[int(1.30 * sr): int(1.80 * sr)] drop_db = 20.0 * np.log10((_rms(during) + 1e-12) / (_rms(before) + 1e-12)) self.assertGreater( drop_db, -0.25, f"default mix should keep accompaniment steady, got {drop_db:.2f} dB drop", ) if __name__ == "__main__": unittest.main()