Level 2: Psychoacoustic first version

This commit is contained in:
2026-02-08 22:53:52 +02:00
parent 9931e3830a
commit ae4ad82136
35 changed files with 2003 additions and 768 deletions
+2 -2
View File
@@ -22,7 +22,7 @@ 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_snr_db import snr_db
from core.aac_utils import snr_db
# Helper "fixtures" for aac_coder_1 / i_aac_coder_1
@@ -222,4 +222,4 @@ 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 > 75.0
assert snr > 80
+1 -1
View File
@@ -17,7 +17,7 @@ from typing import Sequence
import pytest
from core.aac_filterbank import aac_filter_bank, aac_i_filter_bank
from core.aac_snr_db import snr_db
from core.aac_utils import snr_db
from core.aac_types import *
# Helper fixtures for filterbank
@@ -1,117 +0,0 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Filterbank internal (mdct) Tests
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Tests for Filterbank internal MDCT/IMDCT functionality.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
import pytest
from core.aac_filterbank import _imdct, _mdct
from core.aac_types import FloatArray, TimeSignal, MdctCoeffs
def _assert_allclose(a: FloatArray, b: FloatArray, *, 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: MdctCoeffs, x: MdctCoeffs) -> 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 the chosen (non-orthonormal) scaling, g is expected to be close to 2.
"""
rng = np.random.default_rng(0)
K = N // 2
X: MdctCoeffs = rng.normal(size=K).astype(np.float64)
x: TimeSignal = _imdct(X)
X_hat: MdctCoeffs = _mdct(x)
g = _estimate_gain(X_hat, X)
_assert_allclose(X_hat, g * X, rtol=tolerance, atol=tolerance)
_assert_allclose(np.array([g], dtype=np.float64), np.array([2.0], dtype=np.float64), 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)
"""
rng = np.random.default_rng(1)
x: TimeSignal = rng.normal(size=N).astype(np.float64)
y: TimeSignal = rng.normal(size=N).astype(np.float64)
a = 0.37
b = -1.12
left: MdctCoeffs = _mdct(a * x + b * y)
right: MdctCoeffs = 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: MdctCoeffs = rng.normal(size=K).astype(np.float64)
Y: MdctCoeffs = rng.normal(size=K).astype(np.float64)
a = -0.5
b = 2.0
left: TimeSignal = _imdct(a * X + b * Y)
right: TimeSignal = 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: TimeSignal = rng.normal(size=N).astype(np.float64)
X: MdctCoeffs = rng.normal(size=K).astype(np.float64)
X1 = _mdct(x)
x1 = _imdct(X)
assert np.isfinite(X1).all()
assert np.isfinite(x1).all()
+253
View File
@@ -0,0 +1,253 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Psychoacoustic Model Tests
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
#
# Description:
# Contract + sanity tests for the psychoacoustic model (core.aac_psycho).
#
# These tests focus on:
# - output shapes per frame_type (long vs ESH),
# - numerical sanity (finite, non-negative),
# - deterministic behavior,
# - ESH central-region dependency (outer regions must not affect result),
# - basic input validation (length checks).
#
# We intentionally avoid asserting exact numeric values, because the model
# includes FFT operations and table-driven psychoacoustic parameters.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
import pytest
from core.aac_psycho import aac_psycho
from core.aac_types import FrameChannelT, FrameType
# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------
def _make_frames(
*,
kind: str,
amp: float = 1.0,
seed: int = 0,
) -> tuple[FrameChannelT, FrameChannelT, FrameChannelT]:
"""
Create (current, prev1, prev2) 2048-sample frames for one channel.
Parameters
----------
kind : str
"noise" or "tone".
amp : float
Amplitude scaling (applied to all frames).
seed : int
RNG seed for reproducibility (noise case).
Returns
-------
tuple[FrameChannelT, FrameChannelT, FrameChannelT]
Three arrays of shape (2048,), dtype float64.
"""
if kind == "noise":
rng = np.random.default_rng(seed)
x2 = amp * rng.normal(size=2048).astype(np.float64)
x1 = amp * rng.normal(size=2048).astype(np.float64)
x0 = amp * rng.normal(size=2048).astype(np.float64)
return x0, x1, x2
if kind == "tone":
# A simple sinusoid which is identical across frames (highly predictable).
n = np.arange(2048, dtype=np.float64)
f0 = 13.0 # arbitrary normalized-bin-ish tone (not critical for these tests)
tone = amp * np.sin(2.0 * np.pi * f0 * n / 2048.0).astype(np.float64)
return tone, tone.copy(), tone.copy()
raise ValueError(f"Unknown kind: {kind!r}")
def _assert_finite_nonnegative(x: np.ndarray) -> None:
"""Utility assertions for psycho outputs."""
assert np.isfinite(x).all()
# SMR is a ratio of energies, it should not be negative.
assert np.min(x) >= 0.0
# -----------------------------------------------------------------------------
# Shape / contract tests
# -----------------------------------------------------------------------------
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
def test_psycho_long_shapes(frame_type: FrameType) -> None:
"""
Contract test:
For long frame types, psycho returns SMR shape (69,).
"""
x0, x1, x2 = _make_frames(kind="noise", seed=1, amp=1.0)
smr = aac_psycho(x0, frame_type, x1, x2)
assert isinstance(smr, np.ndarray)
assert smr.shape == (69,)
_assert_finite_nonnegative(smr)
def test_psycho_esh_shape() -> None:
"""
Contract test:
For ESH, psycho returns SMR shape (42, 8).
"""
x0, x1, x2 = _make_frames(kind="noise", seed=2, amp=1.0)
smr = aac_psycho(x0, "ESH", x1, x2)
assert isinstance(smr, np.ndarray)
assert smr.shape == (42, 8)
_assert_finite_nonnegative(smr)
def test_psycho_is_deterministic_for_same_inputs() -> None:
"""
Determinism test:
Psycho must return the same output for identical inputs.
"""
x0, x1, x2 = _make_frames(kind="noise", seed=3, amp=1.0)
smr1 = aac_psycho(x0, "OLS", x1, x2)
smr2 = aac_psycho(x0, "OLS", x1, x2)
np.testing.assert_allclose(smr1, smr2, rtol=0.0, atol=0.0)
# -----------------------------------------------------------------------------
# ESH-specific behavior tests
# -----------------------------------------------------------------------------
def test_psycho_esh_ignores_outer_regions() -> None:
"""
Spec-driven behavior test:
In this project, ESH uses only the central region of the 2048-sample frame to
derive the 8 overlapping 256-sample subframes:
start = 448 + 128*j, j=0..7
Therefore, changing samples outside [448, 1600) must not affect the output.
"""
rng = np.random.default_rng(10)
# Build base frames (current and prev1) with identical central region.
center_cur = rng.normal(size=1152).astype(np.float64)
center_prev1 = rng.normal(size=1152).astype(np.float64)
cur_a = np.zeros(2048, dtype=np.float64)
cur_b = np.zeros(2048, dtype=np.float64)
prev1_a = np.zeros(2048, dtype=np.float64)
prev1_b = np.zeros(2048, dtype=np.float64)
cur_a[448:1600] = center_cur
cur_b[448:1600] = center_cur
prev1_a[448:1600] = center_prev1
prev1_b[448:1600] = center_prev1
# Modify only outer regions in the *_b variants.
cur_b[:448] = rng.normal(size=448)
cur_b[1600:] = rng.normal(size=448)
prev1_b[:448] = rng.normal(size=448)
prev1_b[1600:] = rng.normal(size=448)
# prev2 is irrelevant for the chosen ESH history convention; keep it fixed.
prev2 = rng.normal(size=2048).astype(np.float64)
smr_a = aac_psycho(cur_a, "ESH", prev1_a, prev2)
smr_b = aac_psycho(cur_b, "ESH", prev1_b, prev2)
np.testing.assert_allclose(smr_a, smr_b, rtol=0.0, atol=0.0)
def test_psycho_esh_columns_are_not_all_identical_for_random_input() -> None:
"""
Sanity test:
For random input, different ESH subframes should typically produce
different SMR columns (not a strict requirement, but a strong sanity signal).
We check that at least one column differs from another beyond a tiny tolerance.
"""
x0, x1, x2 = _make_frames(kind="noise", seed=11, amp=1.0)
smr = aac_psycho(x0, "ESH", x1, x2)
# Compare column 0 vs column 7; for random signals they should differ.
diff = np.max(np.abs(smr[:, 0] - smr[:, 7]))
assert diff > 1e-12
# -----------------------------------------------------------------------------
# Scaling sanity (avoid fragile numeric targets)
# -----------------------------------------------------------------------------
def test_psycho_long_smr_is_mostly_monotone_with_amplitude() -> None:
"""
Sanity test:
Increasing signal amplitude should not reduce the SMR for the vast majority
of Bark bands.
Due to the use of max(nb, qthr), a small fraction of bands close to the
threshold-in-quiet boundary may violate strict monotonicity. This is expected
behavior, so we test a percentage-based criterion instead of a strict one.
"""
x0, x1, x2 = _make_frames(kind="noise", seed=20, amp=1e3)
y0, y1, y2 = (2.0 * x0, 2.0 * x1, 2.0 * x2)
smr1 = aac_psycho(x0, "OLS", x1, x2)
smr2 = aac_psycho(y0, "OLS", y1, y2)
eps = 1e-12
nondecreasing = np.sum(smr2 + eps >= smr1)
ratio = nondecreasing / smr1.size
# Expect monotonic behavior for the overwhelming majority of bands.
assert ratio >= 0.95
def test_psycho_long_is_approximately_scale_invariant_at_high_level() -> None:
"""
Sanity test (robust):
At high levels, SMR should be approximately scale-invariant for most bands.
Some bands may deviate due to the max(nb, qthr) branch.
"""
x0, x1, x2 = _make_frames(kind="noise", seed=20, amp=1e3)
y0, y1, y2 = (2.0 * x0, 2.0 * x1, 2.0 * x2)
smr1 = aac_psycho(x0, "OLS", x1, x2)
smr2 = aac_psycho(y0, "OLS", y1, y2)
rel = np.abs(smr2 - smr1) / np.maximum(np.abs(smr1), 1e-12)
# Most bands should be close (<= 5%), but allow a small number of outliers.
close = np.sum(rel <= 5e-2)
assert close >= (smr1.size - 2) # allow up to 2 bands to deviate
# -----------------------------------------------------------------------------
# Input validation tests
# -----------------------------------------------------------------------------
def test_psycho_rejects_wrong_lengths() -> None:
"""
Contract test:
aac_psycho requires 2048-sample frames for current/prev1/prev2.
"""
x = np.zeros(2048, dtype=np.float64)
bad = np.zeros(2047, dtype=np.float64)
with pytest.raises(ValueError):
_ = aac_psycho(bad, "OLS", x, x)
with pytest.raises(ValueError):
_ = aac_psycho(x, "OLS", bad, x)
with pytest.raises(ValueError):
_ = aac_psycho(x, "OLS", x, bad)
-98
View File
@@ -1,98 +0,0 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - SNR dB Tests
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Basic tests for SNR calculation utility.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
import pytest
from core.aac_snr_db import snr_db
from core.aac_types import StereoSignal
def test_snr_perfect_reconstruction_returns_inf() -> None:
"""
If x_hat == x_ref exactly, noise power is zero and SNR must be +inf.
"""
rng = np.random.default_rng(0)
x: StereoSignal = rng.normal(size=(1024, 2)).astype(np.float64)
snr = snr_db(x, x)
assert snr == float("inf")
def test_snr_zero_reference_returns_minus_inf() -> None:
"""
If reference signal is identically zero, signal power is zero
and SNR must be -inf (unless noise is also zero, which is degenerate).
"""
x_ref: StereoSignal = np.zeros((1024, 2), dtype=np.float64)
x_hat: StereoSignal = np.ones((1024, 2), dtype=np.float64)
snr = snr_db(x_ref, x_hat)
assert snr == float("-inf")
def test_snr_known_noise_level_matches_expected_value() -> None:
"""
Deterministic test with known signal and noise power.
Let:
x_ref = ones
x_hat = ones + noise
With noise variance sigma^2, expected SNR:
10 * log10(Ps / Pn)
"""
n = 1000
sigma = 0.1
x_ref: StereoSignal = np.ones((n, 2), dtype=np.float64)
noise = sigma * np.ones((n, 2), dtype=np.float64)
x_hat: StereoSignal = x_ref + noise
ps = float(np.sum(x_ref * x_ref))
pn = float(np.sum(noise * noise))
expected = 10.0 * np.log10(ps / pn)
snr = snr_db(x_ref, x_hat)
assert np.isclose(snr, expected, rtol=1e-12, atol=1e-12)
def test_snr_aligns_different_lengths_and_channels() -> None:
"""
The function must:
- align to minimum length
- align to minimum channel count
without crashing.
"""
rng = np.random.default_rng(1)
x_ref: StereoSignal = rng.normal(size=(1000, 2)).astype(np.float64)
x_hat: StereoSignal = rng.normal(size=(800, 1)).astype(np.float64)
snr = snr_db(x_ref, x_hat)
assert np.isfinite(snr)
def test_snr_accepts_1d_inputs() -> None:
"""
1-D inputs must be accepted and treated as single-channel signals.
"""
rng = np.random.default_rng(2)
x_ref = rng.normal(size=1024).astype(np.float64)
x_hat = x_ref + 0.01 * rng.normal(size=1024).astype(np.float64)
snr = snr_db(x_ref, x_hat)
assert np.isfinite(snr)
+329
View File
@@ -0,0 +1,329 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - SNR dB Tests
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# - Basic tests for SNR calculation utility.
# - Contract and sanity tests for TableB219-related utilities.
#
# These tests do NOT validate the numerical correctness of the
# Bark tables themselves (they are given by the AAC spec),
# but instead ensure:
# - correct loading from disk,
# - correct table selection per frame type,
# - internal consistency of band limits,
# - correct caching behavior.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
import pytest
from core.aac_utils import mdct, imdct, snr_db, load_b219_tables, get_table, band_limits
from core.aac_types import *
tolerance = 1e-10
# mdct / imdct
# ------------------------------------------------------------
def _assert_allclose(a: FloatArray, b: FloatArray, *, 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: MdctCoeffs, x: MdctCoeffs) -> 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)
@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 the chosen (non-orthonormal) scaling, g is expected to be close to 2.
"""
rng = np.random.default_rng(0)
K = N // 2
X: MdctCoeffs = rng.normal(size=K).astype(np.float64)
x: TimeSignal = imdct(X)
X_hat: MdctCoeffs = mdct(x)
g = _estimate_gain(X_hat, X)
_assert_allclose(X_hat, g * X, rtol=tolerance, atol=tolerance)
_assert_allclose(np.array([g], dtype=np.float64), np.array([2.0], dtype=np.float64), 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)
"""
rng = np.random.default_rng(1)
x: TimeSignal = rng.normal(size=N).astype(np.float64)
y: TimeSignal = rng.normal(size=N).astype(np.float64)
a = 0.37
b = -1.12
left: MdctCoeffs = mdct(a * x + b * y)
right: MdctCoeffs = 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: MdctCoeffs = rng.normal(size=K).astype(np.float64)
Y: MdctCoeffs = rng.normal(size=K).astype(np.float64)
a = -0.5
b = 2.0
left: TimeSignal = imdct(a * X + b * Y)
right: TimeSignal = 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: TimeSignal = rng.normal(size=N).astype(np.float64)
X: MdctCoeffs = rng.normal(size=K).astype(np.float64)
X1 = mdct(x)
x1 = imdct(X)
assert np.isfinite(X1).all()
assert np.isfinite(x1).all()
# SNR
# ------------------------------------------------------------
def test_snr_perfect_reconstruction_returns_inf() -> None:
"""
If x_hat == x_ref exactly, noise power is zero and SNR must be +inf.
"""
rng = np.random.default_rng(0)
x: StereoSignal = rng.normal(size=(1024, 2)).astype(np.float64)
snr = snr_db(x, x)
assert snr == float("inf")
def test_snr_zero_reference_returns_minus_inf() -> None:
"""
If reference signal is identically zero, signal power is zero
and SNR must be -inf (unless noise is also zero, which is degenerate).
"""
x_ref: StereoSignal = np.zeros((1024, 2), dtype=np.float64)
x_hat: StereoSignal = np.ones((1024, 2), dtype=np.float64)
snr = snr_db(x_ref, x_hat)
assert snr == float("-inf")
def test_snr_known_noise_level_matches_expected_value() -> None:
"""
Deterministic test with known signal and noise power.
Let:
x_ref = ones
x_hat = ones + noise
With noise variance sigma^2, expected SNR:
10 * log10(Ps / Pn)
"""
n = 1000
sigma = 0.1
x_ref: StereoSignal = np.ones((n, 2), dtype=np.float64)
noise = sigma * np.ones((n, 2), dtype=np.float64)
x_hat: StereoSignal = x_ref + noise
ps = float(np.sum(x_ref * x_ref))
pn = float(np.sum(noise * noise))
expected = 10.0 * np.log10(ps / pn)
snr = snr_db(x_ref, x_hat)
assert np.isclose(snr, expected, rtol=1e-12, atol=1e-12)
def test_snr_aligns_different_lengths_and_channels() -> None:
"""
The function must:
- align to minimum length
- align to minimum channel count
without crashing.
"""
rng = np.random.default_rng(1)
x_ref: StereoSignal = rng.normal(size=(1000, 2)).astype(np.float64)
x_hat: StereoSignal = rng.normal(size=(800, 1)).astype(np.float64)
snr = snr_db(x_ref, x_hat)
assert np.isfinite(snr)
def test_snr_accepts_1d_inputs() -> None:
"""
1-D inputs must be accepted and treated as single-channel signals.
"""
rng = np.random.default_rng(2)
x_ref = rng.normal(size=1024).astype(np.float64)
x_hat = x_ref + 0.01 * rng.normal(size=1024).astype(np.float64)
snr = snr_db(x_ref, x_hat)
assert np.isfinite(snr)
# Table219b
# ------------------------------------------------------------
def test_load_b219_tables_returns_expected_keys() -> None:
"""
Contract test:
TableB219.mat must load successfully and expose both tables
required by the psychoacoustic model.
The AAC spec defines:
- B219a: long-frame Bark bands
- B219b: short-frame Bark bands
"""
tables = load_b219_tables()
assert isinstance(tables, dict)
assert "B219a" in tables
assert "B219b" in tables
def test_b219_table_shapes_are_correct() -> None:
"""
Sanity test:
Verify that the Bark tables have the expected number of bands
and sufficient columns.
Expected from AAC spec:
- B219a: 69 bands (long frames)
- B219b: 42 bands (short frames)
- At least 6 columns (as accessed by band_limits()).
"""
tables = load_b219_tables()
B219a = tables["B219a"]
assert B219a.ndim == 2
assert B219a.shape[0] == 69
assert B219a.shape[1] >= 6
B219b = tables["B219b"]
assert B219b.ndim == 2
assert B219b.shape[0] == 42
assert B219b.shape[1] >= 6
def test_get_table_returns_correct_fft_size() -> None:
"""
Interface test:
get_table(frame_type) must return both:
- the correct Bark table
- the correct FFT size N
This mapping is fundamental for the psychoacoustic model.
"""
table_long, N_long = get_table("OLS")
assert N_long == 2048
assert table_long.shape[0] == 69
table_short, N_short = get_table("ESH")
assert N_short == 256
assert table_short.shape[0] == 42
def test_band_limits_are_consistent_for_long_table() -> None:
"""
Sanity test for band limits (long frames):
For each Bark band:
- wlow <= whigh
- frequency indices stay within [0, N/2)
- all returned arrays have consistent lengths
"""
table, N = get_table("OLS")
wlow, whigh, bval, qthr = band_limits(table)
B = table.shape[0]
assert len(wlow) == B
assert len(whigh) == B
assert len(bval) == B
assert len(qthr) == B
for b in range(B):
assert 0 <= wlow[b] <= whigh[b]
assert whigh[b] < N // 2
def test_band_limits_are_consistent_for_short_table() -> None:
"""
Sanity test for band limits (short frames / ESH).
Same invariants as for long frames, but with FFT size N=256.
"""
table, N = get_table("ESH")
wlow, whigh, bval, qthr = band_limits(table)
B = table.shape[0]
assert len(wlow) == B
assert len(whigh) == B
for b in range(B):
assert 0 <= wlow[b] <= whigh[b]
assert whigh[b] < N // 2
def test_b219_tables_are_cached() -> None:
"""
Implementation test:
load_b219_tables() should cache the loaded tables so that
subsequent calls return the same object (identity check).
This avoids repeated disk I/O during psychoacoustic analysis.
"""
t1 = load_b219_tables()
t2 = load_b219_tables()
assert t1 is t2