Level 3: small fixes and report
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -27,13 +27,17 @@ from typing import Optional, Tuple, Union
|
||||
|
||||
import os
|
||||
import soundfile as sf
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from core.aac_types import AACSeq3, StereoSignal
|
||||
from core.aac_coder import aac_coder_3 as core_aac_coder_3
|
||||
from core.aac_coder import aac_read_wav_stereo_48k
|
||||
from core.aac_decoder import aac_decoder_3 as core_aac_decoder_3
|
||||
from core.aac_utils import snr_db
|
||||
from core.aac_utils import snr_db, estimate_lag_mono, match_gain
|
||||
|
||||
# Global variable to "pass" AACSeq3 without changing the demo_aac_e interface.
|
||||
AAC_Seq_3: AACSeq3
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helpers (Level 3 metrics)
|
||||
@@ -90,6 +94,83 @@ def _bitrate_after_from_aacseq(aac_seq_3: AACSeq3, duration_sec: float) -> float
|
||||
return float(total_bits) / float(duration_sec)
|
||||
|
||||
|
||||
def _plot_frame_bitrate_and_compression(
|
||||
aac_seq_3: AACSeq3,
|
||||
wav_path: Union[str, Path],
|
||||
fname_bitrate: Union[str, Path],
|
||||
fname_comp: Union[str, Path],
|
||||
) -> None:
|
||||
"""
|
||||
Compute and plot per-frame bitrate and compression ratio
|
||||
for a Level 3 AAC sequence.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aac_seq_3 : list
|
||||
Output of aac_coder_3 (list of frame dictionaries).
|
||||
wav_path : str or Path
|
||||
Path to original WAV file (PCM 48 kHz stereo).
|
||||
fname_bitrate : str or Path
|
||||
Path to original bitrate per frame plot output file.
|
||||
fname_comp : str or Path
|
||||
Path to original compression per frame plot output file.
|
||||
"""
|
||||
|
||||
# Read WAV metadata
|
||||
info = sf.info(str(wav_path))
|
||||
samplerate = info.samplerate
|
||||
total_samples = info.frames
|
||||
total_duration = total_samples / samplerate
|
||||
|
||||
n_frames = len(aac_seq_3)
|
||||
|
||||
# AAC long-frame hop size is 1024 new samples per frame
|
||||
samples_per_frame = 1024
|
||||
duration_per_frame = samples_per_frame / samplerate
|
||||
|
||||
# Original bitrate (file-based estimate)
|
||||
original_bits = os.path.getsize(wav_path) * 8.0
|
||||
original_bitrate = original_bits / total_duration
|
||||
|
||||
frame_bitrates = []
|
||||
frame_compression = []
|
||||
|
||||
for fr in aac_seq_3:
|
||||
bits = 0
|
||||
bits += len(fr["chl"]["sfc"])
|
||||
bits += len(fr["chl"]["stream"])
|
||||
bits += len(fr["chr"]["sfc"])
|
||||
bits += len(fr["chr"]["stream"])
|
||||
|
||||
bitrate = bits / duration_per_frame
|
||||
compression = original_bitrate / bitrate if bitrate > 0 else np.inf
|
||||
|
||||
frame_bitrates.append(bitrate)
|
||||
frame_compression.append(compression)
|
||||
|
||||
frame_indices = np.arange(n_frames)
|
||||
|
||||
# Plot bitrate per frame and save to file
|
||||
plt.figure(figsize=(6, 3), dpi=300)
|
||||
plt.plot(frame_indices, frame_bitrates)
|
||||
plt.xlabel("Frame index")
|
||||
plt.ylabel("Bitrate (bits/s)")
|
||||
plt.title("Bitrate (per-frame)")
|
||||
plt.tight_layout()
|
||||
plt.savefig(str(fname_bitrate))
|
||||
plt.close()
|
||||
|
||||
# Plot compression ratio per frame and save to file
|
||||
plt.figure(figsize=(6, 3), dpi=300)
|
||||
plt.plot(frame_indices, frame_compression)
|
||||
plt.xlabel("Frame index")
|
||||
plt.ylabel("Compression Ratio")
|
||||
plt.title("Compression Ratio (per-frame)")
|
||||
plt.tight_layout()
|
||||
plt.savefig(str(fname_comp))
|
||||
plt.close()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Public Level 3 API (wrappers)
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -187,20 +268,21 @@ def demo_aac_3(
|
||||
raise ValueError("Input sampling rate must be 48 kHz.")
|
||||
|
||||
# Encode / decode
|
||||
aac_seq_3 = aac_coder_3(filename_in, filename_aac_coded)
|
||||
x_hat = i_aac_coder_3(aac_seq_3, filename_out)
|
||||
global AAC_Seq_3 # pick coder output
|
||||
AAC_Seq_3 = aac_coder_3(filename_in, filename_aac_coded)
|
||||
x_hat = i_aac_coder_3(AAC_Seq_3, filename_out)
|
||||
|
||||
# Optional sanity: ensure output file exists and is readable
|
||||
_, fs_hat = sf.read(str(filename_out), always_2d=True)
|
||||
if int(fs_hat) != 48000:
|
||||
raise ValueError("Decoded output sampling rate must be 48 kHz.")
|
||||
|
||||
# Metrics
|
||||
# Quality metrics
|
||||
s = snr_db(x_ref, x_hat)
|
||||
|
||||
duration = _wav_duration_seconds(filename_in)
|
||||
bitrate_before = _bitrate_before_from_file(filename_in)
|
||||
bitrate_after = _bitrate_after_from_aacseq(aac_seq_3, duration)
|
||||
bitrate_after = _bitrate_after_from_aacseq(AAC_Seq_3, duration)
|
||||
compression = float("inf") if bitrate_after <= 0.0 else (bitrate_before / bitrate_after)
|
||||
|
||||
return float(s), float(bitrate_after), float(compression)
|
||||
@@ -218,20 +300,28 @@ if __name__ == "__main__":
|
||||
# python -m level_3 material/LicorDeCalandraca.wav LicorDeCalandraca_out_l3.wav
|
||||
# or
|
||||
# python -m level_3 material/LicorDeCalandraca.wav LicorDeCalandraca_out_l3.wav aac_seq_3.mat
|
||||
# or
|
||||
# python -m level_3 material/LicorDeCalandraca.wav LicorDeCalandraca_out_l3.wav aac_seq_3.mat bitrate.png compression.png
|
||||
import sys
|
||||
|
||||
if len(sys.argv) not in (3, 4):
|
||||
raise SystemExit("Usage: python -m level_3 <input.wav> <output.wav> [aac_seq_3.mat]")
|
||||
|
||||
if len(sys.argv) not in (3, 4, 5, 6):
|
||||
raise SystemExit(
|
||||
"Usage: python -m level_3 <input.wav> <output.wav> [aac_seq_3.mat] [bitrate_fname] [compression_fname]"
|
||||
)
|
||||
in_wav = Path(sys.argv[1])
|
||||
out_wav = Path(sys.argv[2])
|
||||
aac_mat = Path(sys.argv[3]) if len(sys.argv) == 4 else None
|
||||
fname_bitrate = Path(sys.argv[4]) if len(sys.argv) == 5 else "bitrate_per_frame.png"
|
||||
fname_comp = Path(sys.argv[5]) if len(sys.argv) == 6 else "compression_per_frame.png"
|
||||
|
||||
print(f"Encoding/Decoding {in_wav} to {out_wav}")
|
||||
if aac_mat is not None:
|
||||
print(f"Storing coded sequence to {aac_mat}")
|
||||
|
||||
snr, bitrate, compression = demo_aac_3(in_wav, out_wav, aac_mat)
|
||||
# plot compresion / bitrate
|
||||
_plot_frame_bitrate_and_compression(AAC_Seq_3, in_wav, fname_bitrate, fname_comp)
|
||||
|
||||
print(f"SNR = {snr:.3f} dB")
|
||||
print(f"Bitrate (coded) = {bitrate:.2f} bits/s")
|
||||
print(f"Compression ratio = {compression:.4f}")
|
||||
|
||||
Reference in New Issue
Block a user