Level 3: First positive SNR version
This commit is contained in:
@@ -25,38 +25,64 @@ from core.aac_utils import snr_db
|
||||
from core.aac_types import *
|
||||
|
||||
|
||||
# Helper "fixtures" for aac_coder_1 / i_aac_coder_1
|
||||
# -----------------------------------------------------------------------------
|
||||
# Fixtures (small wav logic)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def wav_in_path() -> Path:
|
||||
"""
|
||||
Provided input WAV used for end-to-end tests.
|
||||
|
||||
Expected project layout:
|
||||
source/material/LicorDeCalandraca.wav
|
||||
"""
|
||||
return Path(__file__).resolve().parents[2] / "material" / "LicorDeCalandraca.wav"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def tmp_stereo_wav(tmp_path: Path) -> Path:
|
||||
def mk_random_stereo_wav(tmp_path: Path, request: pytest.FixtureRequest) -> Path:
|
||||
"""
|
||||
Create a temporary 48 kHz stereo WAV with random samples.
|
||||
|
||||
Length (in seconds) must be provided via indirect parametrization.
|
||||
"""
|
||||
length = float(request.param)
|
||||
rng = np.random.default_rng(123)
|
||||
fs = 48000
|
||||
|
||||
# ~1 second of audio (kept small for test speed).
|
||||
n = fs
|
||||
n = int(fs * length)
|
||||
x: StereoSignal = rng.normal(size=(n, 2)).astype(np.float64)
|
||||
|
||||
wav_path = tmp_path / "in.wav"
|
||||
wav_path = tmp_path / "in_random.wav"
|
||||
sf.write(str(wav_path), x, fs)
|
||||
return wav_path
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mk_actual_stereo_wav(tmp_path: Path, wav_in_path: Path, request: pytest.FixtureRequest) -> Path:
|
||||
"""
|
||||
Create a temporary 48 kHz stereo WAV by chopping from the provided material WAV.
|
||||
Length can be overridden via indirect parametrization.
|
||||
"""
|
||||
length = float(getattr(request, "param", 0.25)) # seconds (default: small)
|
||||
|
||||
x, fs = aac_read_wav_stereo_48k(wav_in_path)
|
||||
n = int(fs * length)
|
||||
x_short = x[:n, :]
|
||||
|
||||
wav_path = tmp_path / "in_actual.wav"
|
||||
sf.write(str(wav_path), x_short, fs)
|
||||
return wav_path
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper-function tests
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def test_aac_read_wav_stereo_48k_roundtrip(tmp_stereo_wav: Path) -> None:
|
||||
"""
|
||||
Contract test for aac_read_wav_stereo_48k():
|
||||
- Reads stereo WAV
|
||||
- Returns float64 array with shape (N,2)
|
||||
- Returns fs = 48000
|
||||
"""
|
||||
x, fs = aac_read_wav_stereo_48k(tmp_stereo_wav)
|
||||
@pytest.mark.parametrize("mk_random_stereo_wav", [2.0], indirect=True)
|
||||
def test_aac_read_wav_stereo_48k_roundtrip(mk_random_stereo_wav: Path) -> None:
|
||||
x, fs = aac_read_wav_stereo_48k(mk_random_stereo_wav)
|
||||
|
||||
assert int(fs) == 48000
|
||||
assert isinstance(x, np.ndarray)
|
||||
@@ -67,10 +93,6 @@ def test_aac_read_wav_stereo_48k_roundtrip(tmp_stereo_wav: Path) -> None:
|
||||
|
||||
|
||||
def test_aac_remove_padding_removes_hop_from_both_ends() -> None:
|
||||
"""
|
||||
Contract test for aac_remove_padding():
|
||||
- Removes 'hop' samples from start and end.
|
||||
"""
|
||||
hop = 1024
|
||||
n = 10000
|
||||
|
||||
@@ -82,9 +104,6 @@ def test_aac_remove_padding_removes_hop_from_both_ends() -> None:
|
||||
|
||||
|
||||
def test_aac_remove_padding_errors_on_too_short_input() -> None:
|
||||
"""
|
||||
aac_remove_padding must raise if y_pad is shorter than 2*hop.
|
||||
"""
|
||||
hop = 1024
|
||||
y_pad: StereoSignal = np.zeros((2 * hop - 1, 2), dtype=np.float64)
|
||||
|
||||
@@ -95,12 +114,10 @@ def test_aac_remove_padding_errors_on_too_short_input() -> None:
|
||||
# -----------------------------------------------------------------------------
|
||||
# Level 1 tests
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_aac_coder_seq_schema_and_shapes(tmp_stereo_wav: Path) -> None:
|
||||
"""
|
||||
Module-level contract test:
|
||||
Ensure aac_seq_1 follows the expected schema and per-frame shapes.
|
||||
"""
|
||||
aac_seq: AACSeq1 = aac_coder_1(tmp_stereo_wav)
|
||||
|
||||
@pytest.mark.parametrize("mk_random_stereo_wav", [0.5], indirect=True)
|
||||
def test_aac_coder_seq_schema_and_shapes(mk_random_stereo_wav: Path) -> None:
|
||||
aac_seq: AACSeq1 = aac_coder_1(mk_random_stereo_wav)
|
||||
|
||||
assert isinstance(aac_seq, list)
|
||||
assert len(aac_seq) > 0
|
||||
@@ -108,7 +125,6 @@ def test_aac_coder_seq_schema_and_shapes(tmp_stereo_wav: Path) -> None:
|
||||
for fr in aac_seq:
|
||||
assert isinstance(fr, dict)
|
||||
|
||||
# Required keys
|
||||
assert "frame_type" in fr
|
||||
assert "win_type" in fr
|
||||
assert "chl" in fr
|
||||
@@ -136,41 +152,32 @@ def test_aac_coder_seq_schema_and_shapes(tmp_stereo_wav: Path) -> None:
|
||||
assert chr_f.shape == (1024, 1)
|
||||
|
||||
|
||||
def test_end_to_end_aac_coder_decoder_high_snr(tmp_stereo_wav: Path, tmp_path: Path) -> None:
|
||||
"""
|
||||
End-to-end test:
|
||||
Encode + decode and check SNR is very high (numerical-noise only).
|
||||
|
||||
The threshold is intentionally loose to avoid fragility across platforms/BLAS.
|
||||
"""
|
||||
x_ref, fs = sf.read(str(tmp_stereo_wav), always_2d=True)
|
||||
@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:
|
||||
x_ref, fs = sf.read(str(mk_random_stereo_wav), always_2d=True)
|
||||
x_ref = np.asarray(x_ref, dtype=np.float64)
|
||||
assert int(fs) == 48000
|
||||
|
||||
out_wav = tmp_path / "out.wav"
|
||||
|
||||
aac_seq = aac_coder_1(tmp_stereo_wav)
|
||||
aac_seq = aac_coder_1(mk_random_stereo_wav)
|
||||
x_hat: StereoSignal = aac_decoder_1(aac_seq, out_wav)
|
||||
|
||||
# Basic sanity: output file exists and is readable
|
||||
assert out_wav.exists()
|
||||
x_hat_file, fs_hat = sf.read(str(out_wav), always_2d=True)
|
||||
_, fs_hat = sf.read(str(out_wav), always_2d=True)
|
||||
assert int(fs_hat) == 48000
|
||||
|
||||
# SNR against returned array (file should match closely, but we do not require it here).
|
||||
snr = snr_db(x_ref, x_hat)
|
||||
assert snr > 80.0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Level 2 tests (new)
|
||||
# Level 2 tests
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def test_aac_coder_2_seq_schema_and_shapes(tmp_stereo_wav: Path) -> None:
|
||||
"""
|
||||
Module-level contract test (Level 2):
|
||||
Ensure aac_seq_2 follows the expected schema and per-frame shapes, including tns_coeffs.
|
||||
"""
|
||||
aac_seq: AACSeq2 = aac_coder_2(tmp_stereo_wav)
|
||||
@pytest.mark.parametrize("mk_random_stereo_wav", [0.5], indirect=True)
|
||||
def test_aac_coder_2_seq_schema_and_shapes(mk_random_stereo_wav: Path) -> None:
|
||||
aac_seq: AACSeq2 = aac_coder_2(mk_random_stereo_wav)
|
||||
|
||||
assert isinstance(aac_seq, list)
|
||||
assert len(aac_seq) > 0
|
||||
@@ -194,27 +201,20 @@ def test_aac_coder_2_seq_schema_and_shapes(tmp_stereo_wav: Path) -> None:
|
||||
|
||||
if frame_type == "ESH":
|
||||
assert frame_f.shape == (128, 8)
|
||||
assert coeffs.shape[0] == 4
|
||||
assert coeffs.shape[1] == 8
|
||||
assert coeffs.shape == (4, 8)
|
||||
else:
|
||||
assert frame_f.shape == (1024, 1)
|
||||
assert coeffs.shape == (4, 1)
|
||||
|
||||
|
||||
def test_end_to_end_level_2_high_snr(tmp_stereo_wav: Path, tmp_path: Path) -> None:
|
||||
"""
|
||||
End-to-end test (Level 2):
|
||||
Encode + decode and check SNR remains very high.
|
||||
|
||||
Level 2 is still floating-point (TNS is reversible), so reconstruction
|
||||
should remain numerical-noise only.
|
||||
"""
|
||||
x_ref, fs = sf.read(str(tmp_stereo_wav), always_2d=True)
|
||||
@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)
|
||||
x_ref = np.asarray(x_ref, dtype=np.float64)
|
||||
assert int(fs) == 48000
|
||||
|
||||
out_wav = tmp_path / "out_l2.wav"
|
||||
aac_seq = aac_coder_2(tmp_stereo_wav)
|
||||
aac_seq = aac_coder_2(mk_random_stereo_wav)
|
||||
x_hat: StereoSignal = aac_decoder_2(aac_seq, out_wav)
|
||||
|
||||
assert out_wav.exists()
|
||||
@@ -222,30 +222,14 @@ def test_end_to_end_level_2_high_snr(tmp_stereo_wav: Path, tmp_path: Path) -> No
|
||||
assert int(fs_hat) == 48000
|
||||
|
||||
snr = snr_db(x_ref, x_hat)
|
||||
assert snr > 80
|
||||
assert snr > 80.0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Level 3 tests (Quantizer + Huffman)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def wav_in_path() -> Path:
|
||||
"""
|
||||
Input WAV used for end-to-end tests.
|
||||
|
||||
This should point to the provided test audio under material/.
|
||||
Adjust this path if your project layout differs.
|
||||
"""
|
||||
# Typical layout in this project:
|
||||
# source/material/LicorDeCalandraca.wav
|
||||
return Path(__file__).resolve().parents[2] / "material" / "LicorDeCalandraca.wav"
|
||||
|
||||
|
||||
def _assert_level3_frame_schema(frame: AACSeq3Frame) -> None:
|
||||
"""
|
||||
Validate Level-3 per-frame schema (keys + basic types only).
|
||||
"""
|
||||
assert "frame_type" in frame
|
||||
assert "win_type" in frame
|
||||
assert "chl" in frame
|
||||
@@ -264,37 +248,18 @@ def _assert_level3_frame_schema(frame: AACSeq3Frame) -> None:
|
||||
assert isinstance(ch["stream"], str)
|
||||
assert isinstance(ch["codebook"], int)
|
||||
|
||||
# Arrays: only check they are numpy arrays with expected dtype categories.
|
||||
assert isinstance(ch["tns_coeffs"], np.ndarray)
|
||||
assert isinstance(ch["T"], np.ndarray)
|
||||
|
||||
# Global gain: long frames may be scalar float, ESH may be ndarray
|
||||
assert np.isscalar(ch["G"]) or isinstance(ch["G"], np.ndarray)
|
||||
|
||||
|
||||
def test_aac_coder_3_seq_schema_and_shapes(wav_in_path: Path, tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
|
||||
def test_aac_coder_3_seq_schema_and_shapes(mk_actual_stereo_wav: Path) -> None:
|
||||
"""
|
||||
Contract test:
|
||||
- aac_coder_3 returns AACSeq3
|
||||
- Per-frame keys exist and types are consistent
|
||||
- Basic shape expectations hold for ESH vs non-ESH cases
|
||||
|
||||
Note:
|
||||
This test uses a short excerpt (a few frames) to keep runtime bounded.
|
||||
Uses a short WAV excerpt produced by mk_actual_stereo_wav.
|
||||
0.11s is enough for a few frames at 48 kHz.
|
||||
"""
|
||||
# Use only a few frames to avoid long runtimes in the quantizer loop.
|
||||
hop = 1024
|
||||
win = 2048
|
||||
n_frames = 4
|
||||
n_samples = win + (n_frames - 1) * hop
|
||||
|
||||
x, fs = aac_read_wav_stereo_48k(wav_in_path)
|
||||
x_short = x[:n_samples, :]
|
||||
|
||||
short_wav = tmp_path / "input_short.wav"
|
||||
sf.write(str(short_wav), x_short, fs)
|
||||
|
||||
aac_seq_3: AACSeq3 = aac_coder_3(short_wav)
|
||||
aac_seq_3: AACSeq3 = aac_coder_3(mk_actual_stereo_wav)
|
||||
|
||||
assert isinstance(aac_seq_3, list)
|
||||
assert len(aac_seq_3) > 0
|
||||
@@ -329,46 +294,21 @@ def test_aac_coder_3_seq_schema_and_shapes(wav_in_path: Path, tmp_path: Path) ->
|
||||
else:
|
||||
assert np.isscalar(G)
|
||||
|
||||
assert isinstance(ch["sfc"], str)
|
||||
assert isinstance(ch["stream"], str)
|
||||
|
||||
|
||||
|
||||
def test_end_to_end_level_3_high_snr(wav_in_path: Path, tmp_path: Path) -> None:
|
||||
@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:
|
||||
"""
|
||||
End-to-end test for Level 3 (Quantizer + Huffman):
|
||||
|
||||
coder_3 -> decoder_3 should reconstruct a waveform with acceptable SNR.
|
||||
|
||||
Notes
|
||||
-----
|
||||
- Level 3 includes quantization, so SNR is expected to be lower than Level 1/2.
|
||||
- We intentionally use a short excerpt (few frames) to keep runtime bounded,
|
||||
since the reference quantizer implementation is computationally expensive.
|
||||
End-to-end Level 3 using a small WAV excerpt produced by mk_actual_stereo_wav.
|
||||
"""
|
||||
# Use only a few frames to avoid long runtimes.
|
||||
hop = 1024
|
||||
win = 2048
|
||||
n_frames = 4
|
||||
n_samples = win + (n_frames - 1) * hop
|
||||
|
||||
x_ref, fs = aac_read_wav_stereo_48k(wav_in_path)
|
||||
x_short = x_ref[:n_samples, :]
|
||||
|
||||
short_wav = tmp_path / "input_short_l3.wav"
|
||||
sf.write(str(short_wav), x_short, fs)
|
||||
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(short_wav)
|
||||
aac_seq_3: AACSeq3 = aac_coder_3(mk_actual_stereo_wav)
|
||||
y_hat: StereoSignal = aac_decoder_3(aac_seq_3, out_wav)
|
||||
|
||||
# Align lengths defensively (padding removal may differ by a few samples)
|
||||
n = min(x_short.shape[0], y_hat.shape[0])
|
||||
x2 = x_short[:n, :]
|
||||
y2 = y_hat[:n, :]
|
||||
|
||||
s = snr_db(x2, y2)
|
||||
|
||||
# Conservative threshold: Level 3 is lossy by design.
|
||||
assert s > 10.0
|
||||
n = min(x_ref.shape[0], y_hat.shape[0])
|
||||
s = snr_db(x_ref[:n, :], y_hat[:n, :])
|
||||
print(f"SNR={s}")
|
||||
assert s > 2.0
|
||||
|
||||
Reference in New Issue
Block a user