Level 2: Psychoacoustic first version
This commit is contained in:
@@ -9,16 +9,8 @@
|
||||
# cchoutou@ece.auth.gr
|
||||
#
|
||||
# Description:
|
||||
# Level 1 AAC encoder orchestration.
|
||||
# Keeps the same functional behavior as the original level_1 implementation:
|
||||
# - Reads WAV via soundfile
|
||||
# - Validates stereo and 48 kHz
|
||||
# - Frames into 2048 samples with hop=1024 and zero padding at both ends
|
||||
# - SSC decision uses next-frame attack detection
|
||||
# - Filterbank analysis (MDCT)
|
||||
# - Stores per-channel spectra in AACSeq1 schema:
|
||||
# * ESH: (128, 8)
|
||||
# * else: (1024, 1)
|
||||
# - Level 1 AAC encoder orchestration.
|
||||
# - Level 2 AAC encoder orchestration.
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -199,6 +191,10 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
|
||||
return aac_seq
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Level 2 encoder
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
|
||||
"""
|
||||
Level-2 AAC encoder (Level 1 + TNS).
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# Imports
|
||||
from typing import Final
|
||||
|
||||
from core.aac_types import WinType
|
||||
|
||||
# Filterbank
|
||||
@@ -28,4 +30,12 @@ WIN_TYPE: WinType = "SIN"
|
||||
# ------------------------------------------------------------
|
||||
PRED_ORDER = 4
|
||||
QUANT_STEP = 0.1
|
||||
QUANT_MAX = 0.7 # 4-bit symmetric with step 0.1 -> clamp to [-0.7, +0.7]
|
||||
QUANT_MAX = 0.7 # 4-bit symmetric with step 0.1 -> clamp to [-0.7, +0.7]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Psycho
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
NMT_DB: Final[float] = 6.0 # Noise Masking Tone (dB)
|
||||
TMN_DB: Final[float] = 18.0 # Tone Masking Noise (dB)
|
||||
@@ -9,16 +9,9 @@
|
||||
# cchoutou@ece.auth.gr
|
||||
#
|
||||
# Description:
|
||||
# Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
# Keeps the same functional behavior as the original level_1 implementation:
|
||||
# - Re-pack per-channel spectra into FrameF expected by aac_i_filter_bank()
|
||||
# - IMDCT synthesis per frame
|
||||
# - Overlap-add with hop=1024
|
||||
# - Remove encoder boundary padding: hop at start and hop at end
|
||||
# - Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
# - Level 2 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
#
|
||||
# Note:
|
||||
# This core module returns the reconstructed samples. Writing to disk is kept
|
||||
# in level_x demos.
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -33,7 +26,7 @@ from core.aac_types import *
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Public helpers (useful for level_x demo wrappers)
|
||||
# Public helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def aac_unpack_seq_channels_to_frame_f(frame_type: FrameType, chl_f: FrameChannelF, chr_f: FrameChannelF) -> FrameF:
|
||||
@@ -109,7 +102,7 @@ def aac_remove_padding(y_pad: StereoSignal, hop: int = 1024) -> StereoSignal:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Level 1 decoder (core)
|
||||
# Level 1 decoder
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoSignal:
|
||||
@@ -167,6 +160,10 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
|
||||
return y
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Level 2 decoder
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoSignal:
|
||||
"""
|
||||
Level-2 AAC decoder (inverse of aac_coder_2).
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from core.aac_utils import mdct, imdct
|
||||
from core.aac_types import *
|
||||
|
||||
from scipy.signal.windows import kaiser
|
||||
@@ -186,74 +187,6 @@ def _window_sequence(frame_type: FrameType, win_type: WinType) -> Window:
|
||||
raise ValueError(f"Invalid frame_type for long window sequence: {frame_type!r}")
|
||||
|
||||
|
||||
def _mdct(s: TimeSignal) -> MdctCoeffs:
|
||||
"""
|
||||
MDCT (direct form) as specified in the assignment.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s : TimeSignal
|
||||
Windowed time samples, 1-D array of length N (N = 2048 or 256).
|
||||
|
||||
Returns
|
||||
-------
|
||||
MdctCoeffs
|
||||
MDCT coefficients, 1-D array of length N/2.
|
||||
|
||||
Definition
|
||||
----------
|
||||
X[k] = 2 * sum_{n=0..N-1} s[n] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
|
||||
where n0 = (N/2 + 1)/2.
|
||||
"""
|
||||
s = np.asarray(s, dtype=np.float64).reshape(-1)
|
||||
N = int(s.shape[0])
|
||||
if N not in (2048, 256):
|
||||
raise ValueError("MDCT input length must be 2048 or 256.")
|
||||
|
||||
n0 = (N / 2.0 + 1.0) / 2.0
|
||||
n = np.arange(N, dtype=np.float64) + n0
|
||||
k = np.arange(N // 2, dtype=np.float64) + 0.5
|
||||
|
||||
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, N/2)
|
||||
X = 2.0 * (s @ C) # (N/2,)
|
||||
return X
|
||||
|
||||
|
||||
def _imdct(X: MdctCoeffs) -> TimeSignal:
|
||||
"""
|
||||
IMDCT (direct form) as specified in the assignment.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : MdctCoeffs
|
||||
MDCT coefficients, 1-D array of length K (K = 1024 or 128).
|
||||
|
||||
Returns
|
||||
-------
|
||||
TimeSignal
|
||||
Reconstructed time samples, 1-D array of length N = 2K.
|
||||
|
||||
Definition
|
||||
----------
|
||||
s[n] = (2/N) * sum_{k=0..N/2-1} X[k] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
|
||||
where n0 = (N/2 + 1)/2.
|
||||
"""
|
||||
X = np.asarray(X, dtype=np.float64).reshape(-1)
|
||||
K = int(X.shape[0])
|
||||
if K not in (1024, 128):
|
||||
raise ValueError("IMDCT input length must be 1024 or 128.")
|
||||
|
||||
N = 2 * K
|
||||
n0 = (N / 2.0 + 1.0) / 2.0
|
||||
|
||||
n = np.arange(N, dtype=np.float64) + n0
|
||||
k = np.arange(K, dtype=np.float64) + 0.5
|
||||
|
||||
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, K)
|
||||
s = (2.0 / N) * (C @ X) # (N,)
|
||||
return s
|
||||
|
||||
|
||||
def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameChannelF:
|
||||
"""
|
||||
ESH analysis for one channel.
|
||||
@@ -279,7 +212,7 @@ def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameCha
|
||||
for j in range(8):
|
||||
start = 448 + 128 * j
|
||||
seg = x_ch[start:start + 256] * wS # (256,)
|
||||
X_esh[:, j] = _mdct(seg) # (128,)
|
||||
X_esh[:, j] = mdct(seg) # (128,)
|
||||
|
||||
return X_esh
|
||||
|
||||
@@ -344,7 +277,7 @@ def _i_filter_bank_esh_channel(X_esh: FrameChannelF, win_type: WinType) -> Frame
|
||||
# Each short IMDCT returns 256 samples. Place them at:
|
||||
# start = 448 + 128*j, j=0..7 (50% overlap)
|
||||
for j in range(8):
|
||||
seg = _imdct(X_esh[:, j]) * wS # (256,)
|
||||
seg = imdct(X_esh[:, j]) * wS # (256,)
|
||||
start = 448 + 128 * j
|
||||
out[start:start + 256] += seg
|
||||
|
||||
@@ -352,7 +285,7 @@ def _i_filter_bank_esh_channel(X_esh: FrameChannelF, win_type: WinType) -> Frame
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Public Function prototypes (Level 1)
|
||||
# Public Function prototypes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -> FrameF:
|
||||
@@ -385,8 +318,8 @@ def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -
|
||||
|
||||
if frame_type in ("OLS", "LSS", "LPS"):
|
||||
w = _window_sequence(frame_type, win_type) # length 2048
|
||||
XL = _mdct(xL * w) # length 1024
|
||||
XR = _mdct(xR * w) # length 1024
|
||||
XL = mdct(xL * w) # length 1024
|
||||
XR = mdct(xR * w) # length 1024
|
||||
out = np.empty((1024, 2), dtype=np.float64)
|
||||
out[:, 0] = XL
|
||||
out[:, 1] = XR
|
||||
@@ -430,8 +363,8 @@ def aac_i_filter_bank(frame_F: FrameF, frame_type: FrameType, win_type: WinType)
|
||||
|
||||
w = _window_sequence(frame_type, win_type)
|
||||
|
||||
xL = _imdct(frame_F[:, 0]) * w
|
||||
xR = _imdct(frame_F[:, 1]) * w
|
||||
xL = imdct(frame_F[:, 0]) * w
|
||||
xR = imdct(frame_F[:, 1]) * w
|
||||
|
||||
out = np.empty((2048, 2), dtype=np.float64)
|
||||
out[:, 0] = xL
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# ------------------------------------------------------------
|
||||
# AAC Coder/Decoder - SNR dB calculator
|
||||
#
|
||||
# Multimedia course at Aristotle University of
|
||||
# Thessaloniki (AUTh)
|
||||
#
|
||||
# Author:
|
||||
# Christos Choutouridis (ΑΕΜ 8997)
|
||||
# cchoutou@ece.auth.gr
|
||||
#
|
||||
# Description:
|
||||
# This module implements SNR calculation in dB
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from core.aac_types import StereoSignal
|
||||
import numpy as np
|
||||
|
||||
def snr_db(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
|
||||
"""
|
||||
Compute overall SNR (dB) over all samples and channels after aligning lengths.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x_ref : StereoSignal
|
||||
Reference stereo stream.
|
||||
x_hat : StereoSignal
|
||||
Reconstructed stereo stream.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
SNR in dB.
|
||||
- Returns +inf if noise power is zero.
|
||||
- Returns -inf if signal power is zero.
|
||||
"""
|
||||
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))
|
||||
@@ -173,7 +173,7 @@ def _stereo_merge(ft_l: FrameType, ft_r: FrameType) -> FrameType:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Public Function prototypes (Level 1)
|
||||
# Public Function prototypes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def aac_SSC(frame_T: FrameT, next_frame_T: FrameT, prev_frame_type: FrameType) -> FrameType:
|
||||
|
||||
@@ -33,6 +33,7 @@ from typing import Tuple
|
||||
import numpy as np
|
||||
from scipy.io import loadmat
|
||||
|
||||
from core.aac_utils import load_b219_tables
|
||||
from core.aac_configuration import PRED_ORDER, QUANT_STEP, QUANT_MAX
|
||||
from core.aac_types import *
|
||||
|
||||
@@ -40,41 +41,6 @@ from core.aac_types import *
|
||||
# Private helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
_B219_CACHE: dict[str, FloatArray] | None = None
|
||||
|
||||
|
||||
def _load_b219_tables() -> dict[str, FloatArray]:
|
||||
"""
|
||||
Load TableB219.mat and cache the contents.
|
||||
|
||||
The project layout guarantees that a 'material' directory is discoverable
|
||||
from the current working directory (tests and level_123 entrypoints).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, FloatArray]
|
||||
Keys:
|
||||
- "B219a": long bands table (for K=1024 MDCT lines)
|
||||
- "B219b": short bands table (for K=128 MDCT lines)
|
||||
"""
|
||||
global _B219_CACHE
|
||||
if _B219_CACHE is not None:
|
||||
return _B219_CACHE
|
||||
|
||||
mat_path = Path("material") / "TableB219.mat"
|
||||
if not mat_path.exists():
|
||||
raise FileNotFoundError("Could not locate material/TableB219.mat in the current working directory.")
|
||||
|
||||
d = loadmat(str(mat_path))
|
||||
if "B219a" not in d or "B219b" not in d:
|
||||
raise ValueError("TableB219.mat missing required variables B219a and/or B219b.")
|
||||
|
||||
_B219_CACHE = {
|
||||
"B219a": np.asarray(d["B219a"], dtype=np.float64),
|
||||
"B219b": np.asarray(d["B219b"], dtype=np.float64),
|
||||
}
|
||||
return _B219_CACHE
|
||||
|
||||
|
||||
def _band_ranges_for_kcount(k_count: int) -> BandRanges:
|
||||
"""
|
||||
@@ -92,7 +58,7 @@ def _band_ranges_for_kcount(k_count: int) -> BandRanges:
|
||||
BandRanges (list[tuple[int, int]])
|
||||
Each tuple is (start_k, end_k) inclusive.
|
||||
"""
|
||||
tables = _load_b219_tables()
|
||||
tables = load_b219_tables()
|
||||
if k_count == 1024:
|
||||
tbl = tables["B219a"]
|
||||
elif k_count == 128:
|
||||
@@ -425,7 +391,7 @@ def _tns_one_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Public Functions (Level 2)
|
||||
# Public Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChannelF, TnsCoeffs]:
|
||||
|
||||
@@ -193,6 +193,25 @@ Bark-band index ranges [start, end] (inclusive) for MDCT lines.
|
||||
Used by TNS to map MDCT indices k to Bark bands.
|
||||
"""
|
||||
|
||||
BarkTable: TypeAlias = FloatArray
|
||||
"""
|
||||
Psychoacoustic Bark band table loaded from TableB219.mat.
|
||||
|
||||
Typical shapes:
|
||||
- Long: (69, 6)
|
||||
- Short: (42, 6)
|
||||
"""
|
||||
|
||||
BandIndexArray: TypeAlias = NDArray[np.int_]
|
||||
"""
|
||||
Array of FFT bin indices per psychoacoustic band.
|
||||
"""
|
||||
|
||||
BandValueArray: TypeAlias = FloatArray
|
||||
"""
|
||||
Per-band psychoacoustic values (e.g. Bark position, thresholds).
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Level 1 AAC sequence payload types
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
# ------------------------------------------------------------
|
||||
# AAC Coder/Decoder - AAC Utilities
|
||||
#
|
||||
# Multimedia course at Aristotle University of
|
||||
# Thessaloniki (AUTh)
|
||||
#
|
||||
# Author:
|
||||
# Christos Choutouridis (ΑΕΜ 8997)
|
||||
# cchoutou@ece.auth.gr
|
||||
#
|
||||
# Description:
|
||||
# Shared utility functions used across AAC encoder/decoder levels.
|
||||
#
|
||||
# This module currently provides:
|
||||
# - MDCT / IMDCT conversions
|
||||
# - Signal-to-Noise Ratio (SNR) computation in dB
|
||||
# - Loading and access helpers for psychoacoustic band tables
|
||||
# (TableB219.mat, Tables B.2.1.9a / B.2.1.9b of the AAC specification)
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
from scipy.io import loadmat
|
||||
from core.aac_types import *
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Global cached data
|
||||
# -----------------------------------------------------------------------------
|
||||
# Cached contents of TableB219.mat to avoid repeated disk I/O.
|
||||
# Keys:
|
||||
# - "B219a": long-window psychoacoustic bands (69 bands, FFT size 2048)
|
||||
# - "B219b": short-window psychoacoustic bands (42 bands, FFT size 256)
|
||||
B219_CACHE: dict[str, BarkTable] | None = None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MDCT / IMDCT
|
||||
# -----------------------------------------------------------------------------
|
||||
def mdct(s: TimeSignal) -> MdctCoeffs:
|
||||
"""
|
||||
MDCT (direct form) as specified in the assignment.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s : TimeSignal
|
||||
Windowed time samples, 1-D array of length N (N = 2048 or 256).
|
||||
|
||||
Returns
|
||||
-------
|
||||
MdctCoeffs
|
||||
MDCT coefficients, 1-D array of length N/2.
|
||||
|
||||
Definition
|
||||
----------
|
||||
X[k] = 2 * sum_{n=0..N-1} s[n] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
|
||||
where n0 = (N/2 + 1)/2.
|
||||
"""
|
||||
s = np.asarray(s, dtype=np.float64).reshape(-1)
|
||||
N = int(s.shape[0])
|
||||
if N not in (2048, 256):
|
||||
raise ValueError("MDCT input length must be 2048 or 256.")
|
||||
|
||||
n0 = (N / 2.0 + 1.0) / 2.0
|
||||
n = np.arange(N, dtype=np.float64) + n0
|
||||
k = np.arange(N // 2, dtype=np.float64) + 0.5
|
||||
|
||||
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, N/2)
|
||||
X = 2.0 * (s @ C) # (N/2,)
|
||||
return X
|
||||
|
||||
|
||||
def imdct(X: MdctCoeffs) -> TimeSignal:
|
||||
"""
|
||||
IMDCT (direct form) as specified in the assignment.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : MdctCoeffs
|
||||
MDCT coefficients, 1-D array of length K (K = 1024 or 128).
|
||||
|
||||
Returns
|
||||
-------
|
||||
TimeSignal
|
||||
Reconstructed time samples, 1-D array of length N = 2K.
|
||||
|
||||
Definition
|
||||
----------
|
||||
s[n] = (2/N) * sum_{k=0..N/2-1} X[k] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
|
||||
where n0 = (N/2 + 1)/2.
|
||||
"""
|
||||
X = np.asarray(X, dtype=np.float64).reshape(-1)
|
||||
K = int(X.shape[0])
|
||||
if K not in (1024, 128):
|
||||
raise ValueError("IMDCT input length must be 1024 or 128.")
|
||||
|
||||
N = 2 * K
|
||||
n0 = (N / 2.0 + 1.0) / 2.0
|
||||
|
||||
n = np.arange(N, dtype=np.float64) + n0
|
||||
k = np.arange(K, dtype=np.float64) + 0.5
|
||||
|
||||
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, K)
|
||||
s = (2.0 / N) * (C @ X) # (N,)
|
||||
return s
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Signal quality metrics
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def snr_db(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
|
||||
"""
|
||||
Compute the overall Signal-to-Noise Ratio (SNR) in dB.
|
||||
|
||||
The SNR is computed over all available samples and channels,
|
||||
after conservatively aligning the two signals to their common
|
||||
length and channel count.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x_ref : StereoSignal
|
||||
Reference (original) signal.
|
||||
Typical shape: (N, 2) for stereo.
|
||||
x_hat : StereoSignal
|
||||
Reconstructed or processed signal.
|
||||
Typical shape: (M, 2) for stereo.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
SNR in dB.
|
||||
- +inf if the noise power is zero (perfect reconstruction).
|
||||
- -inf if the reference signal power is zero.
|
||||
"""
|
||||
x_ref = np.asarray(x_ref, dtype=np.float64)
|
||||
x_hat = np.asarray(x_hat, dtype=np.float64)
|
||||
|
||||
# Ensure 2-D shape: (samples, channels)
|
||||
if x_ref.ndim == 1:
|
||||
x_ref = x_ref.reshape(-1, 1)
|
||||
if x_hat.ndim == 1:
|
||||
x_hat = x_hat.reshape(-1, 1)
|
||||
|
||||
# Align lengths and channel count conservatively
|
||||
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)) # signal power
|
||||
pn = float(np.sum(err * err)) # noise power
|
||||
|
||||
if pn <= 0.0:
|
||||
return float("inf")
|
||||
if ps <= 0.0:
|
||||
return float("-inf")
|
||||
|
||||
return float(10.0 * np.log10(ps / pn))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Psychoacoustic band tables (TableB219.mat)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def load_b219_tables() -> dict[str, BarkTable]:
|
||||
"""
|
||||
Load and cache psychoacoustic band tables from TableB219.mat.
|
||||
|
||||
The assignment/project layout assumes that a 'material' directory
|
||||
is available in the current working directory when running:
|
||||
- tests
|
||||
- level_1 / level_2 / level_3 entrypoints
|
||||
|
||||
This function loads the tables once and caches them for subsequent calls.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, BarkTable]
|
||||
Dictionary with the following entries:
|
||||
- "B219a": long-window psychoacoustic table
|
||||
(69 bands, FFT size 2048 / 1024 spectral lines)
|
||||
- "B219b": short-window psychoacoustic table
|
||||
(42 bands, FFT size 256 / 128 spectral lines)
|
||||
"""
|
||||
global B219_CACHE
|
||||
if B219_CACHE is not None:
|
||||
return B219_CACHE
|
||||
|
||||
mat_path = Path("material") / "TableB219.mat"
|
||||
if not mat_path.exists():
|
||||
raise FileNotFoundError(
|
||||
"Could not locate material/TableB219.mat in the current working directory."
|
||||
)
|
||||
|
||||
data = loadmat(str(mat_path))
|
||||
if "B219a" not in data or "B219b" not in data:
|
||||
raise ValueError(
|
||||
"TableB219.mat missing required variables 'B219a' and/or 'B219b'."
|
||||
)
|
||||
|
||||
B219_CACHE = {
|
||||
"B219a": np.asarray(data["B219a"], dtype=np.float64),
|
||||
"B219b": np.asarray(data["B219b"], dtype=np.float64),
|
||||
}
|
||||
return B219_CACHE
|
||||
|
||||
|
||||
def get_table(frame_type: FrameType) -> tuple[BarkTable, int]:
|
||||
"""
|
||||
Select the appropriate psychoacoustic band table and FFT size
|
||||
based on the AAC frame type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame_type : FrameType
|
||||
AAC frame type ("OLS", "LSS", "ESH", "LPS").
|
||||
|
||||
Returns
|
||||
-------
|
||||
table : BarkTable
|
||||
Psychoacoustic band table:
|
||||
- B219a for long frames
|
||||
- B219b for ESH short subframes
|
||||
N : int
|
||||
FFT size corresponding to the table:
|
||||
- 2048 for long frames
|
||||
- 256 for short frames (ESH)
|
||||
"""
|
||||
tables = load_b219_tables()
|
||||
if frame_type == "ESH":
|
||||
return tables["B219b"], 256
|
||||
return tables["B219a"], 2048
|
||||
|
||||
|
||||
def band_limits(
|
||||
table: BarkTable,
|
||||
) -> tuple[BandIndexArray, BandIndexArray, BandValueArray, BandValueArray]:
|
||||
"""
|
||||
Extract per-band metadata from a TableB2.1.9 psychoacoustic table.
|
||||
|
||||
The column layout follows the provided TableB219.mat file and the
|
||||
AAC specification tables B.2.1.9a / B.2.1.9b.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
table : BarkTable
|
||||
Psychoacoustic band table (B219a or B219b).
|
||||
|
||||
Returns
|
||||
-------
|
||||
wlow : BandIndexArray
|
||||
Lower FFT bin index (inclusive) for each band.
|
||||
whigh : BandIndexArray
|
||||
Upper FFT bin index (inclusive) for each band.
|
||||
bval : BandValueArray
|
||||
Bark-scale (or equivalent) band position values.
|
||||
Used in the spreading function.
|
||||
qthr_db : BandValueArray
|
||||
Threshold in quiet for each band, in dB.
|
||||
"""
|
||||
wlow = table[:, 1].astype(int)
|
||||
whigh = table[:, 2].astype(int)
|
||||
bval = table[:, 4].astype(np.float64)
|
||||
qthr_db = table[:, 5].astype(np.float64)
|
||||
return wlow, whigh, bval, qthr_db
|
||||
Reference in New Issue
Block a user