Init commit with level 1 python source
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
# Adjust the import based on package/module layout.
|
||||
from level_1.level_1 import SSC
|
||||
|
||||
# Helper "fixtures" for SSC
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def _next_frame_no_attack() -> np.ndarray:
|
||||
"""
|
||||
Build a next_frame_T that should NOT trigger ESH detection.
|
||||
|
||||
Uses exact zeros so all s2l are zero and the ESH condition (s2l > 1e-3) cannot hold.
|
||||
"""
|
||||
return np.zeros((2048, 2), dtype=np.float64)
|
||||
|
||||
|
||||
def _next_frame_strong_attack(
|
||||
*,
|
||||
attack_left: bool,
|
||||
attack_right: bool,
|
||||
segment_l: int = 4,
|
||||
baseline: float = 1e-6,
|
||||
burst_amp: float = 1.0,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Build a next_frame_T (2048x2) that should trigger ESH detection on selected channels.
|
||||
|
||||
Spec: ESH if exists l in {1..7} with s2l > 1e-3 AND ds2l > 10.
|
||||
We create:
|
||||
- small baseline energy in all samples (avoids division by zero in ds2l),
|
||||
- a strong burst inside one 128-sample segment l in 1..7.
|
||||
"""
|
||||
assert 1 <= segment_l <= 7
|
||||
x = np.full((2048, 2), baseline, dtype=np.float64)
|
||||
|
||||
a = segment_l * 128
|
||||
b = (segment_l + 1) * 128
|
||||
|
||||
if attack_left:
|
||||
x[a:b, 0] += burst_amp
|
||||
if attack_right:
|
||||
x[a:b, 1] += burst_amp
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def _next_frame_below_s2l_threshold(
|
||||
*,
|
||||
left: bool,
|
||||
right: bool,
|
||||
segment_l: int = 4,
|
||||
impulse_amp: float = 0.01,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Construct a next_frame_T where s2l is below 1e-3, so ESH must NOT be triggered,
|
||||
even if ds2l could be large.
|
||||
|
||||
Put a single impulse of amplitude 'impulse_amp' inside a segment.
|
||||
Energy in the 128-sample segment: s2l ~= impulse_amp^2.
|
||||
With impulse_amp=0.01 => s2l ~= 1e-4 < 1e-3.
|
||||
"""
|
||||
assert 1 <= segment_l <= 7
|
||||
x = np.zeros((2048, 2), dtype=np.float64)
|
||||
|
||||
idx = segment_l * 128 + 10 # inside segment
|
||||
if left:
|
||||
x[idx, 0] = impulse_amp
|
||||
if right:
|
||||
x[idx, 1] = impulse_amp
|
||||
|
||||
return x
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 1) Fixed/mandatory cases (prev frame type forces current type)
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def test_ssc_fixed_cases_prev_lss_and_lps() -> None:
|
||||
"""
|
||||
Spec: if prev was:
|
||||
- LSS => current MUST be ESH
|
||||
- LPS => current MUST be OLS
|
||||
independent of next frame check.
|
||||
"""
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
|
||||
# Even if next frame has a strong attack, LSS must force ESH.
|
||||
next_attack = _next_frame_strong_attack(attack_left=True, attack_right=True)
|
||||
out1 = SSC(frame_t, next_attack, "LSS")
|
||||
assert out1 == "ESH"
|
||||
|
||||
# Even if next frame has a strong attack, LPS must force OLS.
|
||||
out2 = SSC(frame_t, next_attack, "LPS")
|
||||
assert out2 == "OLS"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 2) Cases requiring next-frame ESH prediction (energy/attack computation)
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def test_prev_ols_next_not_esh_returns_ols() -> None:
|
||||
"""
|
||||
Spec: if prev=OLS, current is OLS or LSS.
|
||||
Choose LSS iff (i+1) predicted ESH, else OLS.
|
||||
"""
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
next_t = _next_frame_no_attack()
|
||||
|
||||
out = SSC(frame_t, next_t, "OLS")
|
||||
assert out == "OLS"
|
||||
|
||||
|
||||
def test_prev_ols_next_esh_both_channels_returns_lss() -> None:
|
||||
"""
|
||||
prev=OLS, next predicted ESH (both channels) => per-channel decisions are LSS and LSS
|
||||
and merge table keeps LSS.
|
||||
"""
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
next_t = _next_frame_strong_attack(attack_left=True, attack_right=True)
|
||||
|
||||
out = SSC(frame_t, next_t, "OLS")
|
||||
assert out == "LSS"
|
||||
|
||||
|
||||
def test_prev_ols_next_esh_one_channel_returns_lss() -> None:
|
||||
"""
|
||||
prev=OLS:
|
||||
- one channel predicts ESH => LSS
|
||||
- other channel predicts not ESH => OLS
|
||||
Merge table: OLS + LSS => LSS.
|
||||
"""
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
|
||||
next1_t = _next_frame_strong_attack(attack_left=True, attack_right=False)
|
||||
out1 = SSC(frame_t, next1_t, "OLS")
|
||||
assert out1 == "LSS"
|
||||
|
||||
next2_t = _next_frame_strong_attack(attack_left=False, attack_right=True)
|
||||
out2 = SSC(frame_t, next2_t, "OLS")
|
||||
assert out2 == "LSS"
|
||||
|
||||
|
||||
def test_prev_esh_next_esh_both_channels_returns_esh() -> None:
|
||||
"""
|
||||
prev=ESH:
|
||||
- next predicted ESH => current ESH (per-channel)
|
||||
Merge table: ESH + ESH => ESH.
|
||||
"""
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
next_t = _next_frame_strong_attack(attack_left=True, attack_right=True)
|
||||
|
||||
out = SSC(frame_t, next_t, "ESH")
|
||||
assert out == "ESH"
|
||||
|
||||
|
||||
def test_prev_esh_next_not_esh_both_channels_returns_lps() -> None:
|
||||
"""
|
||||
prev=ESH:
|
||||
- next not predicted ESH => current LPS (per-channel)
|
||||
Merge table: LPS + LPS => LPS.
|
||||
"""
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
next_t = _next_frame_no_attack()
|
||||
|
||||
out = SSC(frame_t, next_t, "ESH")
|
||||
assert out == "LPS"
|
||||
|
||||
|
||||
def test_prev_esh_next_esh_one_channel_merged_is_esh() -> None:
|
||||
"""
|
||||
prev=ESH:
|
||||
- one channel predicts ESH => ESH
|
||||
- other channel predicts not ESH => LPS
|
||||
Merge table: ESH + LPS => ESH.
|
||||
"""
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
|
||||
next1_t = _next_frame_strong_attack(attack_left=True, attack_right=False)
|
||||
out1 = SSC(frame_t, next1_t, "ESH")
|
||||
assert out1 == "ESH"
|
||||
|
||||
next2_t = _next_frame_strong_attack(attack_left=True, attack_right=False)
|
||||
out2 = SSC(frame_t, next2_t, "ESH")
|
||||
assert out2 == "ESH"
|
||||
|
||||
def test_threshold_s2l_must_exceed_1e_3() -> None:
|
||||
"""
|
||||
Spec: next frame is ESH only if s2l > 1e-3 AND ds2l > 10 for some l in 1..7.
|
||||
This test checks the necessity of the s2l threshold:
|
||||
- Create a frame with s2l ~= 1e-4 < 1e-3 (single impulse with amp 0.01).
|
||||
- Expect: not classified as ESH -> for prev=OLS return OLS.
|
||||
"""
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
next_t = _next_frame_below_s2l_threshold(left=True, right=True, impulse_amp=0.01)
|
||||
|
||||
out = SSC(frame_t, next_t, "OLS")
|
||||
assert out == "OLS"
|
||||
@@ -0,0 +1,123 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import soundfile as sf
|
||||
|
||||
from level_1.level_1 import aac_coder_1, i_aac_coder_1
|
||||
|
||||
# Helper "fixtures" for aac_coder_1 / i_aac_coder_1
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def _snr_db(x_ref: np.ndarray, x_hat: np.ndarray) -> float:
|
||||
"""
|
||||
Compute overall SNR (dB) over all samples and channels after aligning lengths.
|
||||
"""
|
||||
x_ref = np.asarray(x_ref, dtype=np.float64)
|
||||
x_hat = np.asarray(x_hat, dtype=np.float64)
|
||||
|
||||
if x_ref.ndim == 1:
|
||||
x_ref = x_ref.reshape(-1, 1)
|
||||
if x_hat.ndim == 1:
|
||||
x_hat = x_hat.reshape(-1, 1)
|
||||
|
||||
n = min(x_ref.shape[0], x_hat.shape[0])
|
||||
c = min(x_ref.shape[1], x_hat.shape[1])
|
||||
|
||||
x_ref = x_ref[:n, :c]
|
||||
x_hat = x_hat[:n, :c]
|
||||
|
||||
err = x_ref - x_hat
|
||||
ps = float(np.sum(x_ref * x_ref))
|
||||
pn = float(np.sum(err * err))
|
||||
|
||||
if pn <= 0.0:
|
||||
return float("inf")
|
||||
if ps <= 0.0:
|
||||
return -float("inf")
|
||||
|
||||
return float(10.0 * np.log10(ps / pn))
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def tmp_stereo_wav(tmp_path: Path) -> Path:
|
||||
"""
|
||||
Create a temporary 48 kHz stereo WAV with random samples.
|
||||
"""
|
||||
rng = np.random.default_rng(123)
|
||||
fs = 48000
|
||||
|
||||
# ~1 second of audio, keep small for test speed
|
||||
n = fs
|
||||
x = rng.normal(size=(n, 2)).astype(np.float64)
|
||||
|
||||
wav_path = tmp_path / "in.wav"
|
||||
sf.write(str(wav_path), x, fs)
|
||||
return wav_path
|
||||
|
||||
|
||||
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 = aac_coder_1(tmp_stereo_wav)
|
||||
|
||||
assert isinstance(aac_seq, list)
|
||||
assert len(aac_seq) > 0
|
||||
|
||||
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
|
||||
assert "chr" in fr
|
||||
|
||||
frame_type = fr["frame_type"]
|
||||
win_type = fr["win_type"]
|
||||
|
||||
assert frame_type in ("OLS", "LSS", "ESH", "LPS")
|
||||
assert win_type in ("SIN", "KBD")
|
||||
|
||||
assert isinstance(fr["chl"], dict)
|
||||
assert isinstance(fr["chr"], dict)
|
||||
assert "frame_F" in fr["chl"]
|
||||
assert "frame_F" in fr["chr"]
|
||||
|
||||
chl_f = np.asarray(fr["chl"]["frame_F"])
|
||||
chr_f = np.asarray(fr["chr"]["frame_F"])
|
||||
|
||||
if frame_type == "ESH":
|
||||
assert chl_f.shape == (128, 8)
|
||||
assert chr_f.shape == (128, 8)
|
||||
else:
|
||||
assert chl_f.shape == (1024, 1)
|
||||
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 module test:
|
||||
Encode + decode and check SNR is very high (numerical-noise only).
|
||||
Threshold is intentionally loose to avoid fragility.
|
||||
"""
|
||||
x_ref, fs = sf.read(str(tmp_stereo_wav), always_2d=True)
|
||||
assert fs == 48000
|
||||
|
||||
out_wav = tmp_path / "out.wav"
|
||||
|
||||
aac_seq = aac_coder_1(tmp_stereo_wav)
|
||||
x_hat = i_aac_coder_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)
|
||||
assert fs_hat == 48000
|
||||
|
||||
# SNR computed against the array returned by i_aac_coder_1 (should match file, but not required)
|
||||
snr = _snr_db(x_ref, x_hat)
|
||||
assert snr > 80.0
|
||||
@@ -0,0 +1,235 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from level_1.level_1 import FrameType, WinType, filter_bank, i_filter_bank
|
||||
|
||||
# Helper "fixtures" for filterbank
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def _ola_reconstruct(x: np.ndarray, frame_types: list[str], win_type: str) -> np.ndarray:
|
||||
"""
|
||||
Analyze-synthesize each frame and overlap-add with hop=1024.
|
||||
x: shape (N,2)
|
||||
frame_types: length K, for frames starting at i*1024
|
||||
"""
|
||||
hop = 1024
|
||||
win = 2048
|
||||
K = len(frame_types)
|
||||
|
||||
y = np.zeros_like(x, dtype=np.float64)
|
||||
|
||||
for i in range(K):
|
||||
start = i * hop
|
||||
frame_t = x[start:start + win, :]
|
||||
frame_f = filter_bank(frame_t, frame_types[i], win_type)
|
||||
frame_t_hat = i_filter_bank(frame_f, frame_types[i], win_type)
|
||||
y[start:start + win, :] += frame_t_hat
|
||||
|
||||
return y
|
||||
|
||||
|
||||
def _snr_db(x: np.ndarray, y: np.ndarray) -> float:
|
||||
err = x - y
|
||||
ps = float(np.sum(x * x))
|
||||
pn = float(np.sum(err * err))
|
||||
if pn <= 0.0:
|
||||
return float("inf")
|
||||
return 10.0 * np.log10(ps / pn)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Forward filterbank tests
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
|
||||
def test_filterbank_shapes_long_sequences(frame_type: FrameType, win_type: WinType) -> None:
|
||||
"""
|
||||
Contract test:
|
||||
For OLS/LSS/LPS, filter_bank returns shape (1024, 2).
|
||||
"""
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
frame_f = filter_bank(frame_t, frame_type, win_type)
|
||||
assert frame_f.shape == (1024, 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
def test_filterbank_shapes_esh(win_type: WinType) -> None:
|
||||
"""
|
||||
Contract test:
|
||||
For ESH, filter_bank returns shape (128, 16).
|
||||
"""
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
frame_f = filter_bank(frame_t, "ESH", win_type)
|
||||
assert frame_f.shape == (128, 16)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
def test_filterbank_channel_isolation_long_sequences(win_type: WinType) -> None:
|
||||
"""
|
||||
Module behavior test:
|
||||
For OLS (representative long-sequence), channels are processed independently:
|
||||
- If right channel is zero and left is random, right spectrum should be near zero.
|
||||
"""
|
||||
rng = np.random.default_rng(0)
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
frame_t[:, 0] = rng.normal(size=2048)
|
||||
|
||||
frame_f = filter_bank(frame_t, "OLS", win_type)
|
||||
|
||||
# Right channel output should be (close to) zero
|
||||
assert np.max(np.abs(frame_f[:, 1])) < 1e-9
|
||||
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
def test_filterbank_channel_isolation_esh(win_type: WinType) -> None:
|
||||
"""
|
||||
Module behavior test:
|
||||
For ESH, channels are processed independently:
|
||||
- If right channel is zero and left is random, all odd columns (right) should be near zero.
|
||||
"""
|
||||
rng = np.random.default_rng(1)
|
||||
frame_t = np.zeros((2048, 2), dtype=np.float64)
|
||||
frame_t[:, 0] = rng.normal(size=2048)
|
||||
|
||||
frame_f = filter_bank(frame_t, "ESH", win_type)
|
||||
|
||||
# Right channel appears in columns 1,3,5,...,15
|
||||
right_cols = frame_f[:, 1::2]
|
||||
assert np.max(np.abs(right_cols)) < 1e-9
|
||||
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
def test_filterbank_esh_ignores_outer_regions(win_type: WinType) -> None:
|
||||
"""
|
||||
Spec-driven behavior test:
|
||||
ESH uses only the central 1152 samples (from 448 to 1599), split into 8 overlapping
|
||||
windows of length 256 with 50% overlap.
|
||||
|
||||
Therefore, changing samples outside [448, 1600) must not affect the output.
|
||||
"""
|
||||
rng = np.random.default_rng(2)
|
||||
|
||||
frame_a = np.zeros((2048, 2), dtype=np.float64)
|
||||
frame_b = np.zeros((2048, 2), dtype=np.float64)
|
||||
|
||||
# Same central region for both frames
|
||||
center = rng.normal(size=(1152, 2))
|
||||
frame_a[448:1600, :] = center
|
||||
frame_b[448:1600, :] = center
|
||||
|
||||
# Modify only the outer regions of frame_b
|
||||
frame_b[0:448, :] = rng.normal(size=(448, 2))
|
||||
frame_b[1600:2048, :] = rng.normal(size=(448, 2))
|
||||
|
||||
fa = filter_bank(frame_a, "ESH", win_type)
|
||||
fb = filter_bank(frame_b, "ESH", win_type)
|
||||
|
||||
np.testing.assert_allclose(fa, fb, rtol=0.0, atol=0.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
def test_filterbank_output_is_finite(win_type: WinType) -> None:
|
||||
"""
|
||||
Sanity test:
|
||||
Output must not contain NaN or inf for representative cases.
|
||||
"""
|
||||
rng = np.random.default_rng(3)
|
||||
frame_t = rng.normal(size=(2048, 2)).astype(np.float64)
|
||||
|
||||
for frame_type in ("OLS", "LSS", "ESH", "LPS"):
|
||||
frame_f = filter_bank(frame_t, frame_type, win_type)
|
||||
assert np.isfinite(frame_f).all()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Reverse i_filterbank tests
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
def test_ifilterbank_shapes_long_sequences(win_type: str) -> None:
|
||||
frame_f = np.zeros((1024, 2), dtype=np.float64)
|
||||
for frame_type in ("OLS", "LSS", "LPS"):
|
||||
frame_t = i_filter_bank(frame_f, frame_type, win_type)
|
||||
assert frame_t.shape == (2048, 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
def test_ifilterbank_shapes_esh(win_type: str) -> None:
|
||||
frame_f = np.zeros((128, 16), dtype=np.float64)
|
||||
frame_t = i_filter_bank(frame_f, "ESH", win_type)
|
||||
assert frame_t.shape == (2048, 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
def test_roundtrip_per_frame_is_finite(win_type: str) -> None:
|
||||
rng = np.random.default_rng(0)
|
||||
frame_t = rng.normal(size=(2048, 2)).astype(np.float64)
|
||||
|
||||
for frame_type in ("OLS", "LSS", "ESH", "LPS"):
|
||||
frame_f = filter_bank(frame_t, frame_type, win_type)
|
||||
frame_t_hat = i_filter_bank(frame_f, frame_type, win_type)
|
||||
assert np.isfinite(frame_t_hat).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
def test_ola_reconstruction_ols_high_snr(win_type: str) -> None:
|
||||
"""
|
||||
Core module-level test:
|
||||
OLS analysis+synthesis with hop=1024 must reconstruct with high SNR
|
||||
in the steady-state region.
|
||||
"""
|
||||
rng = np.random.default_rng(1)
|
||||
|
||||
K = 6
|
||||
N = 1024 * (K + 1)
|
||||
x = rng.normal(size=(N, 2)).astype(np.float64)
|
||||
|
||||
y = _ola_reconstruct(x, ["OLS"] * K, win_type)
|
||||
|
||||
# Exclude edges (first and last hop) where full overlap is not available
|
||||
a = 1024
|
||||
b = N - 1024
|
||||
snr = _snr_db(x[a:b, :], y[a:b, :])
|
||||
assert snr > 50.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
def test_ola_reconstruction_esh_high_snr(win_type: str) -> None:
|
||||
"""
|
||||
ESH analysis+synthesis with hop=1024 must reconstruct with high SNR
|
||||
in the steady-state region.
|
||||
"""
|
||||
rng = np.random.default_rng(2)
|
||||
|
||||
K = 6
|
||||
N = 1024 * (K + 1)
|
||||
x = rng.normal(size=(N, 2)).astype(np.float64)
|
||||
|
||||
y = _ola_reconstruct(x, ["ESH"] * K, win_type)
|
||||
|
||||
a = 1024
|
||||
b = N - 1024
|
||||
snr = _snr_db(x[a:b, :], y[a:b, :])
|
||||
assert snr > 45.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("win_type", ["SIN", "KBD"])
|
||||
def test_ola_reconstruction_transition_sequence(win_type: str) -> None:
|
||||
"""
|
||||
Transition sequence test matching the windowing logic:
|
||||
OLS -> LSS -> ESH -> LPS -> OLS -> OLS
|
||||
"""
|
||||
rng = np.random.default_rng(3)
|
||||
|
||||
frame_types = ["OLS", "LSS", "ESH", "LPS", "OLS", "OLS"]
|
||||
K = len(frame_types)
|
||||
N = 1024 * (K + 1)
|
||||
x = rng.normal(size=(N, 2)).astype(np.float64)
|
||||
|
||||
y = _ola_reconstruct(x, frame_types, win_type)
|
||||
|
||||
a = 1024
|
||||
b = N - 1024
|
||||
snr = _snr_db(x[a:b, :], y[a:b, :])
|
||||
assert snr > 40.0
|
||||
@@ -0,0 +1,102 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from level_1.level_1 import _imdct, _mdct
|
||||
|
||||
# Helper "fixtures" for filterbank internals (MDCT/IMDCT)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def _assert_allclose(a: np.ndarray, b: np.ndarray, *, rtol: float, atol: float) -> None:
|
||||
# Helper for consistent tolerances across tests.
|
||||
np.testing.assert_allclose(a, b, rtol=rtol, atol=atol)
|
||||
|
||||
def _estimate_gain(y: np.ndarray, x: np.ndarray) -> float:
|
||||
"""
|
||||
Estimate scalar gain g such that y ~= g*x in least-squares sense.
|
||||
"""
|
||||
denom = float(np.dot(x, x))
|
||||
if denom == 0.0:
|
||||
return 0.0
|
||||
return float(np.dot(y, x) / denom)
|
||||
|
||||
|
||||
tolerance = 1e-10
|
||||
|
||||
@pytest.mark.parametrize("N", [256, 2048])
|
||||
def test_mdct_imdct_mdct_identity_up_to_gain(N: int) -> None:
|
||||
"""
|
||||
Consistency test in coefficient domain:
|
||||
mdct(imdct(X)) ~= g * X
|
||||
|
||||
For our chosen (non-orthonormal) scaling, g is expected to be close to 2.
|
||||
"""
|
||||
rng = np.random.default_rng(0)
|
||||
K = N // 2
|
||||
|
||||
X = rng.normal(size=K).astype(np.float64)
|
||||
x = _imdct(X)
|
||||
X_hat = _mdct(x)
|
||||
|
||||
g = _estimate_gain(X_hat, X)
|
||||
_assert_allclose(X_hat, g * X, rtol=tolerance, atol=tolerance)
|
||||
_assert_allclose(np.array([g]), np.array([2.0]), rtol=tolerance, atol=tolerance)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("N", [256, 2048])
|
||||
def test_mdct_linearity(N: int) -> None:
|
||||
"""
|
||||
Linearity test:
|
||||
mdct(a*x + b*y) == a*mdct(x) + b*mdct(y)
|
||||
|
||||
This should hold up to numerical error.
|
||||
"""
|
||||
rng = np.random.default_rng(1)
|
||||
x = rng.normal(size=N).astype(np.float64)
|
||||
y = rng.normal(size=N).astype(np.float64)
|
||||
|
||||
a = 0.37
|
||||
b = -1.12
|
||||
|
||||
left = _mdct(a * x + b * y)
|
||||
right = a * _mdct(x) + b * _mdct(y)
|
||||
|
||||
_assert_allclose(left, right, rtol=tolerance, atol=tolerance)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("N", [256, 2048])
|
||||
def test_imdct_linearity(N: int) -> None:
|
||||
"""
|
||||
Linearity test for IMDCT:
|
||||
imdct(a*X + b*Y) == a*imdct(X) + b*imdct(Y)
|
||||
"""
|
||||
rng = np.random.default_rng(2)
|
||||
K = N // 2
|
||||
|
||||
X = rng.normal(size=K).astype(np.float64)
|
||||
Y = rng.normal(size=K).astype(np.float64)
|
||||
|
||||
a = -0.5
|
||||
b = 2.0
|
||||
|
||||
left = _imdct(a * X + b * Y)
|
||||
right = a * _imdct(X) + b * _imdct(Y)
|
||||
|
||||
_assert_allclose(left, right, rtol=tolerance, atol=tolerance)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("N", [256, 2048])
|
||||
def test_mdct_imdct_outputs_are_finite(N: int) -> None:
|
||||
"""
|
||||
Sanity test: no NaN/inf on random inputs.
|
||||
"""
|
||||
rng = np.random.default_rng(3)
|
||||
K = N // 2
|
||||
|
||||
x = rng.normal(size=N).astype(np.float64)
|
||||
X = rng.normal(size=K).astype(np.float64)
|
||||
|
||||
X1 = _mdct(x)
|
||||
x1 = _imdct(X)
|
||||
|
||||
assert np.isfinite(X1).all()
|
||||
assert np.isfinite(x1).all()
|
||||
Reference in New Issue
Block a user