Level 3: A first failed attempt of encoding-decoding -> SNR=0
This commit is contained in:
@@ -19,10 +19,10 @@ import numpy as np
|
||||
import pytest
|
||||
import soundfile as sf
|
||||
|
||||
from core.aac_coder import aac_coder_1, aac_coder_2, aac_read_wav_stereo_48k
|
||||
from core.aac_decoder import aac_decoder_1, aac_decoder_2, aac_remove_padding
|
||||
from core.aac_types import *
|
||||
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_types import *
|
||||
|
||||
|
||||
# Helper "fixtures" for aac_coder_1 / i_aac_coder_1
|
||||
@@ -222,4 +222,153 @@ 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
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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
|
||||
assert "chr" in frame
|
||||
|
||||
for ch_key in ("chl", "chr"):
|
||||
ch = frame[ch_key] # type: ignore[index]
|
||||
assert "tns_coeffs" in ch
|
||||
assert "T" in ch
|
||||
assert "G" in ch
|
||||
assert "sfc" in ch
|
||||
assert "stream" in ch
|
||||
assert "codebook" in ch
|
||||
|
||||
assert isinstance(ch["sfc"], str)
|
||||
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:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
# 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)
|
||||
|
||||
assert isinstance(aac_seq_3, list)
|
||||
assert len(aac_seq_3) > 0
|
||||
|
||||
for fr in aac_seq_3:
|
||||
_assert_level3_frame_schema(fr)
|
||||
|
||||
frame_type = fr["frame_type"]
|
||||
for ch_key in ("chl", "chr"):
|
||||
ch = fr[ch_key] # type: ignore[index]
|
||||
|
||||
tns = np.asarray(ch["tns_coeffs"])
|
||||
if frame_type == "ESH":
|
||||
assert tns.ndim == 2
|
||||
assert tns.shape[1] == 8
|
||||
else:
|
||||
assert tns.ndim == 2
|
||||
assert tns.shape[1] == 1
|
||||
|
||||
T = np.asarray(ch["T"])
|
||||
if frame_type == "ESH":
|
||||
assert T.ndim == 2
|
||||
assert T.shape[1] == 8
|
||||
else:
|
||||
assert T.ndim == 2
|
||||
assert T.shape[1] == 1
|
||||
|
||||
G = ch["G"]
|
||||
if frame_type == "ESH":
|
||||
assert isinstance(G, np.ndarray)
|
||||
assert np.asarray(G).shape == (1, 8)
|
||||
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:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
# 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)
|
||||
|
||||
out_wav = tmp_path / "decoded_level3.wav"
|
||||
|
||||
aac_seq_3: AACSeq3 = aac_coder_3(short_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
|
||||
@@ -0,0 +1,139 @@
|
||||
# ------------------------------------------------------------
|
||||
# AAC Coder/Decoder - Huffman Wrapper Tests (Level 3)
|
||||
#
|
||||
# Multimedia course at Aristotle University of
|
||||
# Thessaloniki (AUTh)
|
||||
#
|
||||
# Author:
|
||||
# Christos Choutouridis (ΑΕΜ 8997)
|
||||
# cchoutou@ece.auth.gr
|
||||
#
|
||||
# Description:
|
||||
# Contract tests for the Huffman coding stage, using the provided
|
||||
# Huffman utilities (material/huff_utils.py).
|
||||
#
|
||||
# The Huffman encoder/decoder itself is GIVEN by the assignment and
|
||||
# is not re-implemented here. These tests only verify that:
|
||||
#
|
||||
# - The wrapper functions (aac_encode_huff / aac_decode_huff) expose
|
||||
# the API described in the assignment.
|
||||
# - Forced codebook selection works as expected (e.g. scalefactors).
|
||||
# - Tuple-based Huffman coding semantics are respected.
|
||||
#
|
||||
# Notes on tuple coding:
|
||||
# Huffman coding operates on tuples of symbols. As a result,
|
||||
# decode(encode(x)) may return extra trailing symbols due to padding.
|
||||
# The AAC decoder always knows the true section length (from band limits)
|
||||
# and truncates accordingly. Therefore, these tests only enforce that
|
||||
# the decoded PREFIX matches the original data.
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from core.aac_huffman import aac_encode_huff, aac_decode_huff
|
||||
from material.huff_utils import load_LUT
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def huff_LUT():
|
||||
"""
|
||||
Load Huffman Look-Up Tables (LUTs) once per test module.
|
||||
|
||||
The LUTs are provided by the assignment (huffCodebooks.mat) via
|
||||
material.huff_utils.load_LUT().
|
||||
"""
|
||||
return load_LUT()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Roundtrip (prefix) tests
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"coeff_sec",
|
||||
[
|
||||
np.array([1, -1, 2, -2, 0, 0, 3], dtype=np.int64),
|
||||
np.array([0, 0, 0, 0], dtype=np.int64),
|
||||
np.array([5], dtype=np.int64),
|
||||
np.array([-3, -3, -3, -3], dtype=np.int64),
|
||||
],
|
||||
)
|
||||
def test_huffman_roundtrip_prefix_matches(
|
||||
coeff_sec: np.ndarray,
|
||||
huff_LUT,
|
||||
) -> None:
|
||||
"""
|
||||
Contract test for Huffman encode/decode.
|
||||
|
||||
Guarantees:
|
||||
- Encoding followed by decoding does not crash.
|
||||
- The decoded output has at least as many symbols as the input.
|
||||
- The prefix of the decoded output matches the original coefficients.
|
||||
|
||||
Rationale:
|
||||
Huffman tuple coding may introduce padding, so exact length equality
|
||||
is NOT required or expected.
|
||||
"""
|
||||
huff_sec, cb = aac_encode_huff(coeff_sec, huff_LUT)
|
||||
dec = aac_decode_huff(huff_sec, cb, huff_LUT)
|
||||
|
||||
if cb == 0:
|
||||
# Codebook 0 represents an all-zero section.
|
||||
assert np.all(coeff_sec == 0)
|
||||
assert dec.size == 0
|
||||
return
|
||||
|
||||
assert dec.size >= coeff_sec.size
|
||||
np.testing.assert_array_equal(dec[: coeff_sec.size], coeff_sec)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Forced codebook tests
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def test_huffman_force_codebook_returns_requested_codebook(huff_LUT) -> None:
|
||||
"""
|
||||
Verify forced codebook selection.
|
||||
|
||||
According to the assignment, scalefactors must be encoded using
|
||||
Huffman codebook 11. This test checks that:
|
||||
- The requested codebook is actually used.
|
||||
- The decoded prefix matches the original scalefactors.
|
||||
"""
|
||||
scalefactors = np.array([10, -2, 1, 0, -1, 3], dtype=np.int64)
|
||||
|
||||
huff_sec, cb = aac_encode_huff(
|
||||
scalefactors,
|
||||
huff_LUT,
|
||||
force_codebook=11,
|
||||
)
|
||||
|
||||
assert cb == 11
|
||||
assert isinstance(huff_sec, str)
|
||||
|
||||
dec = aac_decode_huff(huff_sec, cb, huff_LUT)
|
||||
|
||||
assert dec.size >= scalefactors.size
|
||||
np.testing.assert_array_equal(dec[: scalefactors.size], scalefactors)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Error handling
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def test_huffman_invalid_codebook_raises(huff_LUT) -> None:
|
||||
"""
|
||||
Decoding with an invalid Huffman codebook index must raise an error.
|
||||
"""
|
||||
with pytest.raises(Exception):
|
||||
_ = aac_decode_huff(
|
||||
huff_sec="010101",
|
||||
huff_codebook=99,
|
||||
huff_LUT=huff_LUT,
|
||||
)
|
||||
@@ -0,0 +1,395 @@
|
||||
# ------------------------------------------------------------
|
||||
# AAC Coder/Decoder - Quantizer Tests
|
||||
#
|
||||
# Multimedia course at Aristotle University of
|
||||
# Thessaloniki (AUTh)
|
||||
#
|
||||
# Author:
|
||||
# Christos Choutouridis (ΑΕΜ 8997)
|
||||
# cchoutou@ece.auth.gr
|
||||
#
|
||||
# Description:
|
||||
# Tests for Quantizer / iQuantizer module.
|
||||
#
|
||||
# These tests are deliberately "contract-oriented":
|
||||
# - They validate shapes, dtypes and invariants that downstream stages
|
||||
# (e.g., Huffman coding) depend on.
|
||||
# - They do not attempt to validate psychoacoustic optimality (that would
|
||||
# require a reference implementation and careful numerical baselines).
|
||||
#
|
||||
# Validates:
|
||||
# - I/O shapes for long and ESH modes
|
||||
# - DPCM scalefactor coding consistency (sfc)
|
||||
# - ESH packing order of quantized symbols (128x8 <-> 1024)
|
||||
# - Edge cases (zeros / near silence)
|
||||
# - Sanity (finite outputs, no extreme numerical blow-up)
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from core.aac_quantizer import aac_quantizer, aac_i_quantizer
|
||||
from core.aac_utils import get_table, band_limits
|
||||
from core.aac_types import FrameType
|
||||
|
||||
|
||||
# Small epsilon to avoid divisions by zero in sanity ratios
|
||||
EPS = 1e-12
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper utilities
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def _nbands(frame_type: FrameType) -> int:
|
||||
"""
|
||||
Return number of scalefactor bands for the given frame type.
|
||||
|
||||
This is derived from TableB219 (psycho tables) via aac_utils helpers,
|
||||
so the tests remain consistent even if tables are updated.
|
||||
"""
|
||||
table, _nfft = get_table(frame_type)
|
||||
wlow, _whigh, _bval, _qthr = band_limits(table)
|
||||
return int(len(wlow))
|
||||
|
||||
|
||||
def _make_smr(frame_type: FrameType, seed: int = 0) -> np.ndarray:
|
||||
"""
|
||||
Create a strictly positive SMR array with the correct shape.
|
||||
|
||||
These tests are not about psycho correctness; they only need SMR > 0
|
||||
to avoid division by zero and to make the quantizer's threshold logic
|
||||
behave deterministically.
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
NB = _nbands(frame_type)
|
||||
|
||||
if frame_type == "ESH":
|
||||
# ESH uses 8 short windows, thus SMR has 8 columns.
|
||||
return (1.0 + np.abs(rng.normal(size=(NB, 8)))).astype(np.float64)
|
||||
|
||||
# Long frames: use a column vector (NB, 1).
|
||||
return (1.0 + np.abs(rng.normal(size=(NB, 1)))).astype(np.float64)
|
||||
|
||||
|
||||
def _reconstruct_alpha_from_sfc(sfc: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Reconstruct alpha(b) from DPCM-coded scalefactors sfc(b).
|
||||
|
||||
By definition in the assignment:
|
||||
sfc(0) = alpha(0)
|
||||
alpha(b) = alpha(b-1) + sfc(b) for b > 0
|
||||
|
||||
This reconstruction is useful to validate the internal consistency
|
||||
of the produced scalefactor information.
|
||||
"""
|
||||
sfc = np.asarray(sfc, dtype=np.int64)
|
||||
|
||||
# Long frames: sfc shape (NB, 1)
|
||||
if sfc.ndim == 2 and sfc.shape[1] == 1:
|
||||
NB = sfc.shape[0]
|
||||
alpha = np.zeros((NB,), dtype=np.int64)
|
||||
alpha[0] = int(sfc[0, 0])
|
||||
for b in range(1, NB):
|
||||
alpha[b] = int(alpha[b - 1] + sfc[b, 0])
|
||||
return alpha
|
||||
|
||||
# ESH frames: sfc shape (NB, 8)
|
||||
if sfc.ndim == 2 and sfc.shape[1] == 8:
|
||||
NB = sfc.shape[0]
|
||||
alpha = np.zeros((NB, 8), dtype=np.int64)
|
||||
alpha[0, :] = sfc[0, :]
|
||||
for b in range(1, NB):
|
||||
alpha[b, :] = alpha[b - 1, :] + sfc[b, :]
|
||||
return alpha
|
||||
|
||||
raise ValueError("Unsupported sfc shape.")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Shape / contract tests
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
|
||||
def test_quantizer_shapes_long(frame_type: FrameType) -> None:
|
||||
"""
|
||||
Contract test for long frames:
|
||||
- Input: MDCT coefficients shape (1024, 1)
|
||||
- Output S: always (1024, 1)
|
||||
- Output sfc: (NB, 1)
|
||||
- G: scalar float for long frames
|
||||
- iQuantizer output: (1024, 1)
|
||||
"""
|
||||
NB = _nbands(frame_type)
|
||||
rng = np.random.default_rng(1)
|
||||
|
||||
X = rng.normal(size=(1024, 1)).astype(np.float64)
|
||||
SMR = _make_smr(frame_type, seed=2)
|
||||
|
||||
S, sfc, G = aac_quantizer(X, frame_type, SMR)
|
||||
|
||||
assert S.shape == (1024, 1)
|
||||
assert sfc.shape == (NB, 1)
|
||||
assert isinstance(G, (float, np.floating))
|
||||
|
||||
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
|
||||
assert Xhat.shape == (1024, 1)
|
||||
|
||||
|
||||
def test_quantizer_shapes_esh() -> None:
|
||||
"""
|
||||
Contract test for ESH frames:
|
||||
- Input: MDCT coefficients shape (128, 8)
|
||||
- Output S: packed to (1024, 1)
|
||||
- Output sfc: (NB, 8)
|
||||
- G: array shape (1, 8) for ESH (one gain per short window)
|
||||
- iQuantizer output: (128, 8)
|
||||
"""
|
||||
frame_type: FrameType = "ESH"
|
||||
NB = _nbands(frame_type)
|
||||
rng = np.random.default_rng(3)
|
||||
|
||||
X = rng.normal(size=(128, 8)).astype(np.float64)
|
||||
SMR = _make_smr(frame_type, seed=4)
|
||||
|
||||
S, sfc, G = aac_quantizer(X, frame_type, SMR)
|
||||
|
||||
assert S.shape == (1024, 1)
|
||||
assert sfc.shape == (NB, 8)
|
||||
assert isinstance(G, np.ndarray)
|
||||
assert G.shape == (1, 8)
|
||||
|
||||
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
|
||||
assert Xhat.shape == (128, 8)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# DPCM consistency tests
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
|
||||
def test_quantizer_dpcm_reconstructs_alpha_long(frame_type: FrameType) -> None:
|
||||
"""
|
||||
Verify the DPCM coding rule for long frames.
|
||||
|
||||
The quantizer returns:
|
||||
sfc(0) = alpha(0)
|
||||
sfc(b) = alpha(b) - alpha(b-1), b>0
|
||||
|
||||
Reconstruct alpha from sfc and check:
|
||||
alpha(0) == sfc(0) == G
|
||||
"""
|
||||
rng = np.random.default_rng(5)
|
||||
|
||||
X = rng.normal(size=(1024, 1)).astype(np.float64)
|
||||
SMR = _make_smr(frame_type, seed=6)
|
||||
|
||||
_S, sfc, G = aac_quantizer(X, frame_type, SMR)
|
||||
|
||||
alpha = _reconstruct_alpha_from_sfc(sfc)
|
||||
|
||||
assert int(sfc[0, 0]) == int(alpha[0])
|
||||
assert float(alpha[0]) == float(G)
|
||||
|
||||
|
||||
def test_quantizer_dpcm_reconstructs_alpha_esh() -> None:
|
||||
"""
|
||||
Verify the DPCM coding rule for ESH frames.
|
||||
|
||||
For each short window j:
|
||||
sfc(0, j) = alpha(0, j) == G(0, j)
|
||||
"""
|
||||
frame_type: FrameType = "ESH"
|
||||
rng = np.random.default_rng(7)
|
||||
|
||||
X = rng.normal(size=(128, 8)).astype(np.float64)
|
||||
SMR = _make_smr(frame_type, seed=8)
|
||||
|
||||
_S, sfc, G = aac_quantizer(X, frame_type, SMR)
|
||||
|
||||
alpha = _reconstruct_alpha_from_sfc(sfc)
|
||||
|
||||
assert np.all(alpha[0, :] == sfc[0, :])
|
||||
assert np.all(alpha[0, :] == G.reshape(-1))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ESH packing order test
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def test_quantizer_esh_packing_order_matches_iquantizer_layout() -> None:
|
||||
"""
|
||||
Verify ESH packing order.
|
||||
|
||||
The quantizer outputs S in packed shape (1024, 1). The expected packing
|
||||
is column-major concatenation of the 8 short subframes.
|
||||
|
||||
This test constructs a deterministic input where each subframe column
|
||||
has a distinct constant value. After quantize+inverse-quantize, the
|
||||
reconstructed columns should remain distinguishable in the same order.
|
||||
|
||||
This primarily tests ordering, not exact numerical values.
|
||||
"""
|
||||
frame_type: FrameType = "ESH"
|
||||
NB = _nbands(frame_type)
|
||||
|
||||
# Create 8 distinct subframes: column j is constant (j+1)
|
||||
X = np.zeros((128, 8), dtype=np.float64)
|
||||
for j in range(8):
|
||||
X[:, j] = float(j + 1)
|
||||
|
||||
# Use very large SMR so thresholds are permissive and alpha changes are
|
||||
# minimal. This helps keep the ordering signal strong.
|
||||
SMR = np.ones((NB, 8), dtype=np.float64) * 1e6
|
||||
|
||||
S, sfc, G = aac_quantizer(X, frame_type, SMR)
|
||||
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
|
||||
|
||||
# The average magnitude per column must be increasing with the original order.
|
||||
col_means = np.mean(Xhat, axis=0)
|
||||
assert np.all(np.diff(col_means) > 0.0)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Edge cases: zeros and near-silence
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
|
||||
def test_quantizer_zero_input_long_is_finite(frame_type: FrameType) -> None:
|
||||
"""
|
||||
Edge case: zero MDCT coefficients should not produce NaN/Inf.
|
||||
|
||||
We do not require identity here (quantizer is lossy), but we require
|
||||
the pipeline to remain numerically safe and produce finite outputs.
|
||||
"""
|
||||
NB = _nbands(frame_type)
|
||||
|
||||
X = np.zeros((1024, 1), dtype=np.float64)
|
||||
SMR = np.ones((NB, 1), dtype=np.float64)
|
||||
|
||||
S, sfc, G = aac_quantizer(X, frame_type, SMR)
|
||||
assert np.isfinite(S).all()
|
||||
assert np.isfinite(sfc).all()
|
||||
assert isinstance(G, (float, np.floating))
|
||||
|
||||
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
|
||||
assert np.isfinite(Xhat).all()
|
||||
|
||||
|
||||
def test_quantizer_zero_input_esh_is_finite() -> None:
|
||||
"""
|
||||
Edge case: same as above, for ESH mode.
|
||||
"""
|
||||
frame_type: FrameType = "ESH"
|
||||
NB = _nbands(frame_type)
|
||||
|
||||
X = np.zeros((128, 8), dtype=np.float64)
|
||||
SMR = np.ones((NB, 8), dtype=np.float64)
|
||||
|
||||
S, sfc, G = aac_quantizer(X, frame_type, SMR)
|
||||
assert np.isfinite(S).all()
|
||||
assert np.isfinite(sfc).all()
|
||||
assert np.isfinite(G).all()
|
||||
|
||||
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
|
||||
assert np.isfinite(Xhat).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
|
||||
def test_quantizer_near_silence_long_is_finite(frame_type: FrameType) -> None:
|
||||
"""
|
||||
Edge case: extremely small values.
|
||||
|
||||
This stresses numerical guards (EPS usage) and ensures no invalid operations.
|
||||
"""
|
||||
NB = _nbands(frame_type)
|
||||
|
||||
X = (1e-15 * np.ones((1024, 1), dtype=np.float64))
|
||||
SMR = np.ones((NB, 1), dtype=np.float64)
|
||||
|
||||
S, sfc, G = aac_quantizer(X, frame_type, SMR)
|
||||
assert np.isfinite(S).all()
|
||||
assert np.isfinite(sfc).all()
|
||||
|
||||
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
|
||||
assert np.isfinite(Xhat).all()
|
||||
|
||||
|
||||
def test_quantizer_near_silence_esh_is_finite() -> None:
|
||||
"""
|
||||
Edge case: extremely small values, ESH mode.
|
||||
"""
|
||||
frame_type: FrameType = "ESH"
|
||||
NB = _nbands(frame_type)
|
||||
|
||||
X = (1e-15 * np.ones((128, 8), dtype=np.float64))
|
||||
SMR = np.ones((NB, 8), dtype=np.float64)
|
||||
|
||||
S, sfc, G = aac_quantizer(X, frame_type, SMR)
|
||||
assert np.isfinite(S).all()
|
||||
assert np.isfinite(sfc).all()
|
||||
assert np.isfinite(G).all()
|
||||
|
||||
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
|
||||
assert np.isfinite(Xhat).all()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Sanity: avoid catastrophic numerical blow-up
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
|
||||
def test_quantizer_sanity_no_extreme_blowup_long(frame_type: FrameType) -> None:
|
||||
"""
|
||||
Loose sanity guard.
|
||||
|
||||
The quantizer is lossy, but it should not produce reconstructions with
|
||||
catastrophic peak/energy growth compared to the input.
|
||||
"""
|
||||
NB = _nbands(frame_type)
|
||||
rng = np.random.default_rng(11)
|
||||
|
||||
X = rng.normal(size=(1024, 1)).astype(np.float64)
|
||||
SMR = np.ones((NB, 1), dtype=np.float64) * 10.0
|
||||
|
||||
S, sfc, G = aac_quantizer(X, frame_type, SMR)
|
||||
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
|
||||
|
||||
in_peak = float(np.max(np.abs(X)))
|
||||
out_peak = float(np.max(np.abs(Xhat)))
|
||||
peak_ratio = out_peak / (in_peak + EPS)
|
||||
|
||||
in_energy = float(np.sum(X * X))
|
||||
out_energy = float(np.sum(Xhat * Xhat))
|
||||
energy_ratio = out_energy / (in_energy + EPS)
|
||||
|
||||
# Very loose thresholds: only catch severe regressions.
|
||||
assert peak_ratio < 100.0
|
||||
assert energy_ratio < 1e4
|
||||
|
||||
|
||||
def test_quantizer_sanity_no_extreme_blowup_esh() -> None:
|
||||
"""
|
||||
Same loose sanity guard for ESH mode.
|
||||
"""
|
||||
frame_type: FrameType = "ESH"
|
||||
NB = _nbands(frame_type)
|
||||
rng = np.random.default_rng(12)
|
||||
|
||||
X = rng.normal(size=(128, 8)).astype(np.float64)
|
||||
SMR = np.ones((NB, 8), dtype=np.float64) * 10.0
|
||||
|
||||
S, sfc, G = aac_quantizer(X, frame_type, SMR)
|
||||
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
|
||||
|
||||
in_peak = float(np.max(np.abs(X)))
|
||||
out_peak = float(np.max(np.abs(Xhat)))
|
||||
peak_ratio = out_peak / (in_peak + EPS)
|
||||
|
||||
in_energy = float(np.sum(X * X))
|
||||
out_energy = float(np.sum(Xhat * Xhat))
|
||||
energy_ratio = out_energy / (in_energy + EPS)
|
||||
|
||||
assert peak_ratio < 100.0
|
||||
assert energy_ratio < 1e4
|
||||
@@ -16,7 +16,7 @@ from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from core.aac_ssc import aac_SSC
|
||||
from core.aac_ssc import aac_ssc
|
||||
from core.aac_types import FrameT
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -117,10 +117,10 @@ def test_ssc_fixed_cases_prev_lss_and_lps() -> None:
|
||||
|
||||
next_attack = _next_frame_strong_attack(attack_left=True, attack_right=True)
|
||||
|
||||
out1 = aac_SSC(frame_t, next_attack, "LSS")
|
||||
out1 = aac_ssc(frame_t, next_attack, "LSS")
|
||||
assert out1 == "ESH"
|
||||
|
||||
out2 = aac_SSC(frame_t, next_attack, "LPS")
|
||||
out2 = aac_ssc(frame_t, next_attack, "LPS")
|
||||
assert out2 == "OLS"
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ def test_prev_ols_next_not_esh_returns_ols() -> None:
|
||||
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
|
||||
next_t = _next_frame_no_attack()
|
||||
|
||||
out = aac_SSC(frame_t, next_t, "OLS")
|
||||
out = aac_ssc(frame_t, next_t, "OLS")
|
||||
assert out == "OLS"
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ def test_prev_ols_next_esh_both_channels_returns_lss() -> None:
|
||||
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
|
||||
next_t = _next_frame_strong_attack(attack_left=True, attack_right=True)
|
||||
|
||||
out = aac_SSC(frame_t, next_t, "OLS")
|
||||
out = aac_ssc(frame_t, next_t, "OLS")
|
||||
assert out == "LSS"
|
||||
|
||||
|
||||
@@ -165,11 +165,11 @@ def test_prev_ols_next_esh_one_channel_returns_lss() -> None:
|
||||
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
|
||||
|
||||
next1_t = _next_frame_strong_attack(attack_left=True, attack_right=False)
|
||||
out1 = aac_SSC(frame_t, next1_t, "OLS")
|
||||
out1 = aac_ssc(frame_t, next1_t, "OLS")
|
||||
assert out1 == "LSS"
|
||||
|
||||
next2_t = _next_frame_strong_attack(attack_left=False, attack_right=True)
|
||||
out2 = aac_SSC(frame_t, next2_t, "OLS")
|
||||
out2 = aac_ssc(frame_t, next2_t, "OLS")
|
||||
assert out2 == "LSS"
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ def test_prev_esh_next_esh_both_channels_returns_esh() -> None:
|
||||
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
|
||||
next_t = _next_frame_strong_attack(attack_left=True, attack_right=True)
|
||||
|
||||
out = aac_SSC(frame_t, next_t, "ESH")
|
||||
out = aac_ssc(frame_t, next_t, "ESH")
|
||||
assert out == "ESH"
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ def test_prev_esh_next_not_esh_both_channels_returns_lps() -> None:
|
||||
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
|
||||
next_t = _next_frame_no_attack()
|
||||
|
||||
out = aac_SSC(frame_t, next_t, "ESH")
|
||||
out = aac_ssc(frame_t, next_t, "ESH")
|
||||
assert out == "LPS"
|
||||
|
||||
|
||||
@@ -209,11 +209,11 @@ def test_prev_esh_next_esh_one_channel_merged_is_esh() -> None:
|
||||
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
|
||||
|
||||
next1_t = _next_frame_strong_attack(attack_left=True, attack_right=False)
|
||||
out1 = aac_SSC(frame_t, next1_t, "ESH")
|
||||
out1 = aac_ssc(frame_t, next1_t, "ESH")
|
||||
assert out1 == "ESH"
|
||||
|
||||
next2_t = _next_frame_strong_attack(attack_left=False, attack_right=True)
|
||||
out2 = aac_SSC(frame_t, next2_t, "ESH")
|
||||
out2 = aac_ssc(frame_t, next2_t, "ESH")
|
||||
assert out2 == "ESH"
|
||||
|
||||
|
||||
@@ -230,5 +230,5 @@ def test_threshold_s_must_exceed_1e_3() -> None:
|
||||
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
|
||||
next_t = _next_frame_below_s_threshold(left=True, right=True, impulse_amp=0.01)
|
||||
|
||||
out = aac_SSC(frame_t, next_t, "OLS")
|
||||
out = aac_ssc(frame_t, next_t, "OLS")
|
||||
assert out == "OLS"
|
||||
@@ -26,6 +26,7 @@ from core.aac_configuration import PRED_ORDER, QUANT_MAX, QUANT_STEP
|
||||
from core.aac_tns import aac_tns, aac_i_tns
|
||||
from core.aac_types import *
|
||||
|
||||
EPS = 1e-12
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper utilities
|
||||
@@ -194,3 +195,132 @@ def test_tns_outputs_are_finite() -> None:
|
||||
out_esh, coeffs_esh = aac_tns(frame_F_esh, "ESH")
|
||||
assert np.isfinite(out_esh).all()
|
||||
assert np.isfinite(coeffs_esh).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
|
||||
def test_tns_zero_input_is_identity_long(frame_type: FrameType) -> None:
|
||||
"""
|
||||
Edge case: zero MDCT coefficients should remain zero after TNS and iTNS.
|
||||
This checks that no NaN/Inf appears and the pipeline is numerically safe.
|
||||
"""
|
||||
frame_F_in = np.zeros((1024, 1), dtype=np.float64)
|
||||
|
||||
frame_F_tns, tns_coeffs = aac_tns(frame_F_in, frame_type)
|
||||
assert np.isfinite(frame_F_tns).all()
|
||||
assert np.isfinite(tns_coeffs).all()
|
||||
assert np.all(frame_F_tns == 0.0)
|
||||
|
||||
frame_F_hat = aac_i_tns(frame_F_tns, frame_type, tns_coeffs)
|
||||
assert np.isfinite(frame_F_hat).all()
|
||||
assert np.all(frame_F_hat == 0.0)
|
||||
|
||||
|
||||
def test_tns_zero_input_is_identity_esh() -> None:
|
||||
"""
|
||||
Edge case: zero MDCT coefficients should remain zero for ESH too.
|
||||
"""
|
||||
frame_F_in = np.zeros((128, 8), dtype=np.float64)
|
||||
|
||||
frame_F_tns, tns_coeffs = aac_tns(frame_F_in, "ESH")
|
||||
assert np.isfinite(frame_F_tns).all()
|
||||
assert np.isfinite(tns_coeffs).all()
|
||||
assert np.all(frame_F_tns == 0.0)
|
||||
|
||||
frame_F_hat = aac_i_tns(frame_F_tns, "ESH", tns_coeffs)
|
||||
assert np.isfinite(frame_F_hat).all()
|
||||
assert np.all(frame_F_hat == 0.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
|
||||
def test_tns_near_silence_is_finite_and_roundtrips(frame_type: FrameType) -> None:
|
||||
"""
|
||||
Edge case: extremely small values should not cause NaN/Inf,
|
||||
and round-trip should remain close.
|
||||
"""
|
||||
frame_F_in = (1e-15 * np.ones((1024, 1), dtype=np.float64))
|
||||
|
||||
frame_F_tns, tns_coeffs = aac_tns(frame_F_in, frame_type)
|
||||
assert np.isfinite(frame_F_tns).all()
|
||||
assert np.isfinite(tns_coeffs).all()
|
||||
|
||||
frame_F_hat = aac_i_tns(frame_F_tns, frame_type, tns_coeffs)
|
||||
assert np.isfinite(frame_F_hat).all()
|
||||
|
||||
np.testing.assert_allclose(frame_F_hat, frame_F_in, rtol=1e-6, atol=1e-12)
|
||||
|
||||
|
||||
def test_tns_near_silence_esh_is_finite_and_roundtrips() -> None:
|
||||
"""
|
||||
Near-silence test for ESH mode.
|
||||
"""
|
||||
frame_F_in = (1e-15 * np.ones((128, 8), dtype=np.float64))
|
||||
|
||||
frame_F_tns, tns_coeffs = aac_tns(frame_F_in, "ESH")
|
||||
assert np.isfinite(frame_F_tns).all()
|
||||
assert np.isfinite(tns_coeffs).all()
|
||||
|
||||
frame_F_hat = aac_i_tns(frame_F_tns, "ESH", tns_coeffs)
|
||||
assert np.isfinite(frame_F_hat).all()
|
||||
|
||||
np.testing.assert_allclose(frame_F_hat, frame_F_in, rtol=1e-6, atol=1e-12)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
|
||||
def test_tns_accepts_flat_vector_shape_long(frame_type: FrameType) -> None:
|
||||
"""
|
||||
Contract test: for non-ESH, aac_tns must accept input shape (1024,)
|
||||
in addition to (1024, 1), and preserve the shape convention.
|
||||
"""
|
||||
rng = np.random.default_rng(7)
|
||||
frame_F_in = rng.normal(size=(1024,)).astype(np.float64)
|
||||
|
||||
frame_F_out, tns_coeffs = aac_tns(frame_F_in, frame_type)
|
||||
|
||||
assert frame_F_out.shape == (1024,)
|
||||
assert tns_coeffs.shape == (PRED_ORDER, 1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
|
||||
def test_tns_does_not_explode_peak_or_energy_long(frame_type: FrameType) -> None:
|
||||
"""
|
||||
Sanity: TNS should not cause extreme peak/energy blow-up on typical inputs.
|
||||
This is a loose guard to catch regressions.
|
||||
"""
|
||||
rng = np.random.default_rng(8)
|
||||
frame_F_in = rng.normal(size=(1024, 1)).astype(np.float64)
|
||||
|
||||
in_peak = float(np.max(np.abs(frame_F_in)))
|
||||
in_energy = float(np.sum(frame_F_in * frame_F_in))
|
||||
|
||||
frame_F_out, _ = aac_tns(frame_F_in, frame_type)
|
||||
|
||||
out_peak = float(np.max(np.abs(frame_F_out)))
|
||||
out_energy = float(np.sum(frame_F_out * frame_F_out))
|
||||
|
||||
peak_ratio = out_peak / (in_peak + EPS)
|
||||
energy_ratio = out_energy / (in_energy + EPS)
|
||||
|
||||
assert peak_ratio < 50.0
|
||||
assert energy_ratio < 2500.0
|
||||
|
||||
|
||||
def test_tns_does_not_explode_peak_or_energy_esh() -> None:
|
||||
"""
|
||||
Sanity: same blow-up guard for ESH mode.
|
||||
"""
|
||||
rng = np.random.default_rng(9)
|
||||
frame_F_in = rng.normal(size=(128, 8)).astype(np.float64)
|
||||
|
||||
in_peak = float(np.max(np.abs(frame_F_in)))
|
||||
in_energy = float(np.sum(frame_F_in * frame_F_in))
|
||||
|
||||
frame_F_out, _ = aac_tns(frame_F_in, "ESH")
|
||||
|
||||
out_peak = float(np.max(np.abs(frame_F_out)))
|
||||
out_energy = float(np.sum(frame_F_out * frame_F_out))
|
||||
|
||||
peak_ratio = out_peak / (in_peak + EPS)
|
||||
energy_ratio = out_energy / (in_energy + EPS)
|
||||
|
||||
assert peak_ratio < 50.0
|
||||
assert energy_ratio < 2500.0
|
||||
|
||||
Reference in New Issue
Block a user