Level 3: small fixes and report

This commit is contained in:
2026-02-22 01:39:12 +02:00
parent 6b1ebc3cfe
commit 17228f8539
36 changed files with 4660 additions and 154 deletions
+2 -21
View File
@@ -111,21 +111,6 @@ def _thresholds_from_smr(
return T
def _normalize_global_gain(G: GlobalGain) -> float | FloatArray:
"""
Normalize GlobalGain to match AACChannelFrameF3["G"] type:
- long: return float
- ESH: return float64 ndarray of shape (1, 8)
"""
if np.isscalar(G):
return float(G)
G_arr = np.asarray(G)
if G_arr.size == 1:
return float(G_arr.reshape(-1)[0])
return np.asarray(G_arr, dtype=np.float64)
# -----------------------------------------------------------------------------
# Public helpers (useful for level_x demo wrappers)
# -----------------------------------------------------------------------------
@@ -476,10 +461,6 @@ def aac_coder_3(
S_L, sfc_L, G_L = aac_quantizer(chl_f_tns, frame_type, SMR_L)
S_R, sfc_R, G_R = aac_quantizer(chr_f_tns, frame_type, SMR_R)
# Normalize G types for AACSeq3 schema (float | float64 ndarray).
G_Ln = _normalize_global_gain(G_L)
G_Rn = _normalize_global_gain(G_R)
# Huffman-code ONLY the DPCM differences for b>0.
# sfc[0] corresponds to alpha(0)=G and is stored separately in the frame.
sfc_L_dpcm = np.asarray(sfc_L, dtype=np.int64)[1:, ...]
@@ -503,7 +484,7 @@ def aac_coder_3(
"chl": {
"tns_coeffs": np.asarray(chl_tns_coeffs, dtype=np.float64),
"T": np.asarray(T_L, dtype=np.float64),
"G": G_Ln,
"G": G_L,
"sfc": sfc_L_stream,
"stream": mdct_L_stream,
"codebook": int(cb_L),
@@ -511,7 +492,7 @@ def aac_coder_3(
"chr": {
"tns_coeffs": np.asarray(chr_tns_coeffs, dtype=np.float64),
"T": np.asarray(T_R, dtype=np.float64),
"G": G_Rn,
"G": G_R,
"sfc": sfc_R_stream,
"stream": mdct_R_stream,
"codebook": int(cb_R),
+3 -3
View File
@@ -42,7 +42,7 @@ def _nbands(frame_type: FrameType) -> int:
# Public helpers
# -----------------------------------------------------------------------------
def aac_unpack_seq_channels_to_frame_f(frame_type: FrameType, chl_f: FrameChannelF, chr_f: FrameChannelF) -> FrameF:
def aac_unpack_seq_channels(frame_type: FrameType, chl_f: FrameChannelF, chr_f: FrameChannelF) -> FrameF:
"""
Re-pack per-channel spectra from the Level-1 AACSeq1 schema into the stereo
FrameF container expected by aac_i_filter_bank().
@@ -167,7 +167,7 @@ def aac_decoder_1(
chl_f = np.asarray(fr["chl"]["frame_F"], dtype=np.float64)
chr_f = np.asarray(fr["chr"]["frame_F"], dtype=np.float64)
frame_f: FrameF = aac_unpack_seq_channels_to_frame_f(frame_type, chl_f, chr_f)
frame_f: FrameF = aac_unpack_seq_channels(frame_type, chl_f, chr_f)
frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type) # (2048, 2)
start = i * hop
@@ -427,7 +427,7 @@ def aac_decoder_3(
X_R = aac_i_tns(Xq_R, frame_type, tns_R)
# Re-pack to stereo container and inverse filterbank
frame_f = aac_unpack_seq_channels_to_frame_f(frame_type, np.asarray(X_L), np.asarray(X_R))
frame_f = aac_unpack_seq_channels(frame_type, np.asarray(X_L), np.asarray(X_R))
frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type)
start = i * hop
+8 -8
View File
@@ -189,7 +189,7 @@ def _predictability(
# Band-domain aggregation
# -----------------------------------------------------------------------------
def _band_energy_and_weighted_predictability(
def _band_energy_and_pred(
r: FloatArray,
c: FloatArray,
wlow: BandIndexArray,
@@ -238,7 +238,7 @@ def _band_energy_and_weighted_predictability(
return e_b, c_num_b
def _psycho_one_window(
def _psycho_window(
time_x: FrameChannelT,
prev1_x: FrameChannelT,
prev2_x: FrameChannelT,
@@ -288,7 +288,7 @@ def _psycho_one_window(
c_w = _predictability(r, phi, r_m1, phi_m1, r_m2, phi_m2)
# Aggregate into psycho bands
e_b, c_num_b = _band_energy_and_weighted_predictability(r, c_w, wlow, whigh)
e_b, c_num_b = _band_energy_and_pred(r, c_w, wlow, whigh)
# Spread energies and predictability across bands:
# ecb(b) = sum_bb e(bb) * S(bb, b)
@@ -333,7 +333,7 @@ def _psycho_one_window(
# ESH window slicing (match filterbank conventions)
# -----------------------------------------------------------------------------
def _esh_subframes_256(x_2048: FrameChannelT) -> list[FrameChannelT]:
def _esh_subframes(x_2048: FrameChannelT) -> list[FrameChannelT]:
"""
Extract the 8 overlapping 256-sample short windows used by AAC ESH.
@@ -408,7 +408,7 @@ def aac_psycho(
# Long frame types: compute one SMR vector (69 bands)
if frame_type != "ESH":
return _psycho_one_window(frame_T, frame_T_prev_1, frame_T_prev_2, N=N, table=table)
return _psycho_window(frame_T, frame_T_prev_1, frame_T_prev_2, N=N, table=table)
# ESH: compute 8 SMR vectors (42 bands each), one per short subframe.
#
@@ -419,8 +419,8 @@ def aac_psycho(
#
# This matches the "within-frame history" convention commonly used in
# simplified psycho models for ESH.
cur_subs = _esh_subframes_256(frame_T)
prev1_subs = _esh_subframes_256(frame_T_prev_1)
cur_subs = _esh_subframes(frame_T)
prev1_subs = _esh_subframes(frame_T_prev_1)
B = int(table.shape[0]) # expected 42
smr_out = np.zeros((B, 8), dtype=np.float64)
@@ -436,6 +436,6 @@ def aac_psycho(
x_m1 = cur_subs[j - 1]
x_m2 = cur_subs[j - 2]
smr_out[:, j] = _psycho_one_window(cur_subs[j], x_m1, x_m2, N=256, table=table)
smr_out[:, j] = _psycho_window(cur_subs[j], x_m1, x_m2, N=256, table=table)
return smr_out
+6 -6
View File
@@ -35,7 +35,7 @@ MAX_SF_DELTA:int = 60
# -----------------------------------------------------------------------------
# Helpers: ESH packing/unpacking (128x8 <-> 1024x1)
# -----------------------------------------------------------------------------
def _esh_pack_to_1024(x_128x8: FloatArray) -> FloatArray:
def _esh_pack(x_128x8: FloatArray) -> FloatArray:
"""
Pack ESH coefficients (128 x 8) into a single long vector (1024 x 1).
@@ -58,7 +58,7 @@ def _esh_pack_to_1024(x_128x8: FloatArray) -> FloatArray:
return x_128x8.reshape(1024, 1, order="F")
def _esh_unpack_from_1024(x_1024x1: FloatArray) -> FloatArray:
def _esh_unpack(x_1024x1: FloatArray) -> FloatArray:
"""
Unpack a packed ESH vector (1024 elements) back to shape (128, 8).
@@ -226,7 +226,7 @@ def _band_energy(x: FloatArray, lo: int, hi: int) -> float:
return float(np.sum(sec * sec))
def _threshold_T_from_SMR(
def _psychoacoustic_threshold(
X: FloatArray,
SMR_col: FloatArray,
bands: list[tuple[int, int]],
@@ -425,7 +425,7 @@ def aac_quantizer(
SMRj = SMR[:, j].reshape(NB)
# Compute psychoacoustic threshold T(b) for this subframe
T = _threshold_T_from_SMR(Xj, SMRj, bands)
T = _psychoacoustic_threshold(Xj, SMRj, bands)
# Frame-wise initial estimate alpha_hat (Equation 14)
alpha_hat = _initial_alpha_hat(Xj)
@@ -482,7 +482,7 @@ def aac_quantizer(
raise ValueError(f"For non-ESH, SMR must have shape ({NB},) or ({NB}, 1).")
# Compute psychoacoustic threshold T(b) for the long frame
T = _threshold_T_from_SMR(Xv, SMRv, bands)
T = _psychoacoustic_threshold(Xv, SMRv, bands)
# Frame-wise initial estimate alpha_hat (Equation 14)
alpha_hat = _initial_alpha_hat(Xv)
@@ -566,7 +566,7 @@ def aac_i_quantizer(
if sfc.shape != (NB, 8):
raise ValueError(f"For ESH, sfc must have shape ({NB}, 8).")
S_128x8 = _esh_unpack_from_1024(S_flat)
S_128x8 = _esh_unpack(S_flat)
Xrec = np.zeros((128, 8), dtype=np.float64)
+6 -6
View File
@@ -39,7 +39,7 @@ from core.aac_types import *
# -----------------------------------------------------------------------------
def _band_ranges_for_kcount(k_count: int) -> BandRanges:
def _band_ranges(k_count: int) -> BandRanges:
"""
Return Bark band index ranges [start, end] (inclusive) for the given MDCT line count.
@@ -66,7 +66,7 @@ def _band_ranges_for_kcount(k_count: int) -> BandRanges:
start = tbl[:, 1].astype(int)
end = tbl[:, 2].astype(int)
ranges: list[tuple[int, int]] = [(int(s), int(e)) for s, e in zip(start, end)]
ranges: BandRanges = [(int(s), int(e)) for s, e in zip(start, end)]
for s, e in ranges:
if s < 0 or e < s or e >= k_count:
@@ -117,7 +117,7 @@ def _compute_sw(x: MdctCoeffs) -> MdctCoeffs:
x = np.asarray(x, dtype=np.float64).reshape(-1)
k_count = int(x.shape[0])
bands = _band_ranges_for_kcount(k_count)
bands = _band_ranges(k_count)
sw = np.zeros(k_count, dtype=np.float64)
for s, e in bands:
@@ -347,7 +347,7 @@ def _apply_itns_iir(y: MdctCoeffs, a_q: MdctCoeffs) -> MdctCoeffs:
return x_hat
def _tns_one_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
def _tns_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
"""
TNS for a single MDCT vector (one long frame or one short subframe).
@@ -430,7 +430,7 @@ def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChan
a_out = np.empty((PRED_ORDER, 8), dtype=np.float64)
for j in range(8):
y[:, j], a_out[:, j] = _tns_one_vector(x[:, j])
y[:, j], a_out[:, j] = _tns_vector(x[:, j])
return y, a_out
@@ -443,7 +443,7 @@ def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChan
else:
raise ValueError('For non-ESH, frame_F_in must have shape (1024,) or (1024, 1).')
y_vec, a_q = _tns_one_vector(x_vec)
y_vec, a_q = _tns_vector(x_vec)
if out_shape == (1024,):
y_out = y_vec
+36
View File
@@ -163,6 +163,42 @@ def snr_db(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
return float(10.0 * np.log10(ps / pn))
def estimate_lag_mono(x_ref: TimeSignal, x_hat: TimeSignal, max_lag=4096):
"""
Estimate time lag between two mono signals.
Returns lag (positive means x_hat delayed).
"""
n = min(len(x_ref), len(x_hat))
x_ref = x_ref[:n]
x_hat = x_hat[:n]
corr = np.correlate(x_ref, x_hat, mode='full')
lags = np.arange(-n + 1, n)
center = n - 1
lo = max(0, center - max_lag)
hi = min(len(corr), center + max_lag + 1)
best = lo + int(np.argmax(corr[lo:hi]))
return int(lags[best])
def match_gain(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Least-squares gain g that best maps x_hat -> x_ref.
"""
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
r = x_ref[:n, :c].reshape(-1).astype(np.float64)
h = x_hat[:n, :c].reshape(-1).astype(np.float64)
denom = float(np.dot(h, h))
if denom <= 0.0:
return 1.0
return float(np.dot(r, h) / denom)
# -----------------------------------------------------------------------------
# Psychoacoustic band tables (TableB219.mat)
# -----------------------------------------------------------------------------