Level 3: small fixes and report

This commit is contained in:
2026-02-22 01:39:12 +02:00
parent 6b1ebc3cfe
commit 17228f8539
36 changed files with 4660 additions and 154 deletions
+133 -3
View File
@@ -21,7 +21,7 @@ import soundfile as sf
from core.aac_coder import aac_coder_1, aac_coder_2, aac_coder_3, aac_read_wav_stereo_48k
from core.aac_decoder import aac_decoder_1, aac_decoder_2, aac_decoder_3, aac_remove_padding
from core.aac_utils import snr_db
from core.aac_utils import snr_db, estimate_lag_mono, match_gain
from core.aac_types import *
@@ -151,6 +151,29 @@ def test_aac_coder_seq_schema_and_shapes(mk_random_stereo_wav: Path) -> None:
assert chl_f.shape == (1024, 1)
assert chr_f.shape == (1024, 1)
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_level_1_gain_close_to_one(
mk_actual_stereo_wav: Path,
tmp_path: Path,
) -> None:
"""
Guardrail: decoded signal should not have a large global gain mismatch.
"""
x_ref, fs = aac_read_wav_stereo_48k(mk_actual_stereo_wav)
assert int(fs) == 48000
out_wav = tmp_path / "decoded_level3.wav"
aac_seq_1: AACSeq1 = aac_coder_1(mk_actual_stereo_wav)
y_hat: StereoSignal = aac_decoder_1(aac_seq_1, out_wav)
n = min(x_ref.shape[0], y_hat.shape[0])
x_ref = x_ref[:n, :]
y_hat = y_hat[:n, :]
g = match_gain(x_ref, y_hat)
# print (f"g = {g}")
# Allow some slack but catch big scaling regressions
assert 0.75 <= g <= 1.25
@pytest.mark.parametrize("mk_random_stereo_wav", [0.5], indirect=True)
def test_end_to_end_aac_coder_decoder_high_snr(mk_random_stereo_wav: Path, tmp_path: Path) -> None:
@@ -207,6 +230,30 @@ def test_aac_coder_2_seq_schema_and_shapes(mk_random_stereo_wav: Path) -> None:
assert coeffs.shape == (4, 1)
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_level_2_gain_close_to_one(
mk_actual_stereo_wav: Path,
tmp_path: Path,
) -> None:
"""
Guardrail: decoded signal should not have a large global gain mismatch.
"""
x_ref, fs = aac_read_wav_stereo_48k(mk_actual_stereo_wav)
assert int(fs) == 48000
out_wav = tmp_path / "decoded_level3.wav"
aac_seq_2: AACSeq2 = aac_coder_2(mk_actual_stereo_wav)
y_hat: StereoSignal = aac_decoder_2(aac_seq_2, out_wav)
n = min(x_ref.shape[0], y_hat.shape[0])
x_ref = x_ref[:n, :]
y_hat = y_hat[:n, :]
g = match_gain(x_ref, y_hat)
# print (f"g = {g}")
# Allow some slack but catch big scaling regressions
assert 0.75 <= g <= 1.25
@pytest.mark.parametrize("mk_random_stereo_wav", [0.5], indirect=True)
def test_end_to_end_level_2_high_snr(mk_random_stereo_wav: Path, tmp_path: Path) -> None:
x_ref, fs = sf.read(str(mk_random_stereo_wav), always_2d=True)
@@ -294,6 +341,90 @@ def test_aac_coder_3_seq_schema_and_shapes(mk_actual_stereo_wav: Path) -> None:
else:
assert np.isscalar(G)
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_level_3_estimated_lag(
mk_actual_stereo_wav: Path,
tmp_path: Path,
) -> None:
"""
Check that the estimated lag between reference and decoded
signal is small (zero), to prevent catastrophic misalignment.
"""
x_ref, fs = aac_read_wav_stereo_48k(mk_actual_stereo_wav)
assert int(fs) == 48000
out_wav = tmp_path / "decoded_level3.wav"
aac_seq_3: AACSeq3 = aac_coder_3(mk_actual_stereo_wav)
y_hat: StereoSignal = aac_decoder_3(aac_seq_3, out_wav)
# Use only common length
n = min(x_ref.shape[0], y_hat.shape[0])
x_ref = x_ref[:n, :]
y_hat = y_hat[:n, :]
lag_L = estimate_lag_mono(x_ref[:, 0], y_hat[:, 0], max_lag=4096)
lag_R = estimate_lag_mono(x_ref[:, 1], y_hat[:, 1], max_lag=4096)
# We allow zero latency
assert abs(lag_L) == 0
assert abs(lag_R) == 0
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_level_3_not_lr_swapped(
mk_actual_stereo_wav: Path,
tmp_path: Path,
) -> None:
"""
Ensure the decoded stereo channels are not swapped.
If channels are swapped, SNR against the original drops a lot,
while SNR against swapped reference increases.
"""
x_ref, fs = aac_read_wav_stereo_48k(mk_actual_stereo_wav)
assert int(fs) == 48000
out_wav = tmp_path / "decoded_level3.wav"
aac_seq_3: AACSeq3 = aac_coder_3(mk_actual_stereo_wav)
y_hat: StereoSignal = aac_decoder_3(aac_seq_3, out_wav)
n = min(x_ref.shape[0], y_hat.shape[0])
x_ref = x_ref[:n, :]
y_hat = y_hat[:n, :]
snr_normal = snr_db(x_ref, y_hat)
snr_swapped = snr_db(x_ref[:, [1, 0]], y_hat)
# If the decoded output were swapped, snr_swapped would be significantly higher.
assert snr_normal >= snr_swapped
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_level_3_gain_close_to_one(
mk_actual_stereo_wav: Path,
tmp_path: Path,
) -> None:
"""
Guardrail: decoded signal should not have a large global gain mismatch.
"""
x_ref, fs = aac_read_wav_stereo_48k(mk_actual_stereo_wav)
assert int(fs) == 48000
out_wav = tmp_path / "decoded_level3.wav"
aac_seq_3: AACSeq3 = aac_coder_3(mk_actual_stereo_wav)
y_hat: StereoSignal = aac_decoder_3(aac_seq_3, out_wav)
n = min(x_ref.shape[0], y_hat.shape[0])
x_ref = x_ref[:n, :]
y_hat = y_hat[:n, :]
g = match_gain(x_ref, y_hat)
# Allow some slack but catch big scaling regressions
assert 0.75 <= g <= 1.25
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_end_to_end_level_3_high_snr(mk_actual_stereo_wav: Path, tmp_path: Path) -> None:
@@ -310,5 +441,4 @@ def test_end_to_end_level_3_high_snr(mk_actual_stereo_wav: Path, tmp_path: Path)
n = min(x_ref.shape[0], y_hat.shape[0])
s = snr_db(x_ref[:n, :], y_hat[:n, :])
# print(f" with SNR={s}")
assert s > 7.0
assert s > 8.0