oxideav_ac4/decoder.rs
1//! Foundation AC-4 decoder.
2//!
3//! Given a packet that carries either a full `ac4_syncframe()` (the
4//! TS/RTP form) or a bare `raw_ac4_frame()` payload (the ISO BMFF MP4
5//! sample form), the decoder:
6//!
7//! 1. Scans for the `0xAC40` / `0xAC41` sync word; if not found, treats
8//! the full packet as a bare payload.
9//! 2. Runs [`toc::parse_ac4_toc`] to extract the channel count, effective
10//! sample rate and frame length.
11//! 3. Emits an `AudioFrame` full of zero S16 samples with the correct
12//! shape.
13//!
14//! This is not a real AC-4 decoder — decoding the ASF / A-SPX / ASF-A2
15//! substream coefficient streams is spec work measured in weeks. What
16//! it *does* give us is a clean path for the rest of the oxideav
17//! pipeline (demuxer → decoder → filter → output) to run end-to-end
18//! against real AC-4 fixtures without panics, plus a parsed
19//! [`toc::Ac4FrameInfo`] surface for downstream tooling.
20
21use oxideav_core::Decoder;
22#[cfg(test)]
23use oxideav_core::TimeBase;
24use oxideav_core::{AudioFrame, CodecId, CodecParameters, Error, Frame, Packet, Result};
25
26use crate::{acpl_synth, asf, aspx, mdct, qmf, ssf, ssf_synth, sync, toc};
27
28pub fn make_decoder(params: &CodecParameters) -> Result<Box<dyn Decoder>> {
29 Ok(Box::new(Ac4Decoder::new(params)))
30}
31
32pub struct Ac4Decoder {
33 codec_id: CodecId,
34 /// Channel count hint supplied by the container (CodecParameters).
35 /// Used as a fallback when the TOC's channel-mode code is one of
36 /// the reserved/escape values.
37 hint_channels: u16,
38 /// Sample-rate hint from the container.
39 hint_sample_rate: u32,
40 pending: Option<Packet>,
41 eof: bool,
42 /// Last parsed frame info — exposed for downstream inspection.
43 pub last_info: Option<toc::Ac4FrameInfo>,
44 /// Last parsed substream tool summary (first substream of the last
45 /// decoded frame). `None` when the TOC didn't expose a usable size
46 /// for the substream (e.g. single-substream frame where
47 /// `b_size_present == 0`).
48 pub last_substream: Option<asf::Ac4SubstreamInfo>,
49 /// Per-channel overlap-add state (length = transform_length samples).
50 /// Keyed by channel index; resized on transform-length change.
51 overlap: Vec<Vec<f32>>,
52 /// Transform length of the previous frame (for overlap sizing).
53 prev_transform_length: u32,
54 /// Per-channel A-SPX persistent state — noise generator
55 /// `noise_idx_prev` (§5.7.6.4.3 Pseudocode 103), tone generator
56 /// `sine_idx_prev` (§5.7.6.4.4 Pseudocode 105), and the
57 /// `sine_idx_sb_prev` / `tsg_ptr_prev` / `num_atsg_sig_prev` bundle
58 /// that Pseudocode 92 consults. Grown on demand as channels decode.
59 aspx_ext_state: Vec<aspx::AspxChannelExtState>,
60 /// Per-substream A-CPL persistent state for the channel-pair
61 /// element (§5.7.7.5 Pseudocode 115). Only one substream is wired
62 /// in the foundation decoder; multichannel `ASPX_ACPL_3` would carry
63 /// a vector keyed by substream index.
64 acpl_state: acpl_synth::AcplSubstreamState,
65 /// Per-substream state for the 5_X `ASPX_ACPL_1` / `ASPX_ACPL_2`
66 /// pair pipeline (§5.7.7.6.1 Pseudocode 117). Carries the pair
67 /// decorrelator + QMF analysis/synthesis banks across frames.
68 acpl_5x_pair_state: acpl_synth::Acpl5xPairPcmState,
69 /// Per-substream state for the 5_X `ASPX_ACPL_3` multichannel
70 /// synthesis pipeline (§5.7.7.6.2 Pseudocode 118). Carries the
71 /// D0/D1/D2 + ducker IIR state + differential-decode rolling sums
72 /// across frames.
73 acpl_5x_mch_state: acpl_synth::Acpl5xMchPcmState,
74 /// Per-channel SSF synthesis state — RNGs, predictor lag history,
75 /// subband-predictor spec/env buffers, and the previous block's
76 /// `f_spec[]` latch. Grown on demand as channels decode SSF.
77 ssf_synth_state: Vec<ssf_synth::SsfSynthState>,
78 /// Per-channel SSF *walker* state — the bitstream-side dither /
79 /// noise RNG, `prev_pred_lag_idx`, `last_num_bands`, and the
80 /// `env_prev[]` snapshot of raw delta symbols. Hoisted onto the
81 /// decoder in round 32 so RNG continuity (Pseudocodes 54-57) is
82 /// preserved across frame boundaries; pre-r32 the walker built a
83 /// fresh per-frame state and dropped it. Grown on demand to match
84 /// the channel count seen on the latest frame.
85 ssf_walker_state: Vec<ssf::SsfChannelState>,
86}
87
88/// Phase-1 result of [`Ac4Decoder::aspx_extend_to_qmf`]:
89/// `(qmf_matrix, sbx, sbz)` — the post-extension QMF matrix and the
90/// (sb0, sb1) range needed by the §5.7.5 companding tool.
91type AspxQmfPhase1 = (aspx::QmfMatrix, u32, u32);
92
93/// One per-channel entry consumed by
94/// [`Ac4Decoder::extend_5x_channels_with_sync_companding`].
95type SyncCompandingChannelEntry<'a> = (
96 usize, // output slot (0..=4)
97 &'a [f32], // pcm_in
98 &'a aspx::FiveXAspxTrailer, // trailer
99 &'a aspx::FiveXAspxChannelTrailer, // channel trailer
100 &'a aspx::AspxConfig, // aspx config
101 Option<u32>, // sb0 override (acpl_qmf_band)
102);
103
104/// One per-channel entry consumed by [`Ac4Decoder::extend_5x_entries`]:
105/// `(slot, pcm_f32, trailer_pair)` where `trailer_pair` is
106/// `(trailer, is_secondary)` — `None` when the channel has no trailer
107/// (passthrough case).
108type FiveXChannelEntry<'a> = (usize, Vec<f32>, Option<(&'a aspx::FiveXAspxTrailer, bool)>);
109
110/// Round 45: per-channel input bundle for the stereo-CPE M=2 synced
111/// companding helper [`Ac4Decoder::extend_stereo_cpe_pair_with_sync_companding`].
112/// Mirrors the per-channel arguments of [`Ac4Decoder::aspx_extend_pcm`]
113/// (the un-trailerised stereo-CPE form used by the primary / secondary
114/// dispatch path) so the two-channel cohort can run phase-1 → synced
115/// companding apply → phase-2 in lockstep.
116struct StereoCpeChannelInput<'a> {
117 /// Decoder-local channel index used to pick the right
118 /// `aspx_ext_state[ch_index]` carry-over (0 for primary, 1 for
119 /// secondary on a 2-channel CPE).
120 ch_index: usize,
121 /// IMDCT'd low-band PCM for this channel; the helper runs forward
122 /// QMF + HF generation + envelope adjustment + companding +
123 /// inverse QMF on this buffer.
124 pcm_in: &'a [f32],
125 /// `aspx_framing()` for this channel (per-channel in stereo CPE).
126 framing: Option<&'a aspx::AspxFraming>,
127 /// `aspx_data_sig` Huffman envelopes for this channel.
128 sig: Option<&'a [aspx::AspxHuffEnv]>,
129 /// `aspx_data_noise` Huffman envelopes for this channel.
130 noise: Option<&'a [aspx::AspxHuffEnv]>,
131 /// `aspx_qmode_env` quant-step for this channel's envelopes.
132 qmode: Option<aspx::AspxQuantStep>,
133 /// Per-envelope sign of the dpcm directionality (`f` flag in the
134 /// spec): `true` = freq-direction, `false` = time-direction.
135 delta_dir: Option<&'a aspx::AspxDeltaDir>,
136 /// `aspx_hfgen_iwc.add_harmonic[ch]` for tone injection.
137 add_harmonic: Option<&'a [bool]>,
138 /// `aspx_hfgen_iwc.tna_mode[ch]` for the chirp + α0 + α1 TNS body.
139 tna_mode: Option<&'a [u8]>,
140}
141
142impl Ac4Decoder {
143 pub fn new(params: &CodecParameters) -> Self {
144 Self {
145 codec_id: params.codec_id.clone(),
146 hint_channels: params.channels.unwrap_or(2),
147 hint_sample_rate: params.sample_rate.unwrap_or(48_000),
148 pending: None,
149 eof: false,
150 last_info: None,
151 last_substream: None,
152 overlap: Vec::new(),
153 prev_transform_length: 0,
154 aspx_ext_state: Vec::new(),
155 acpl_state: acpl_synth::AcplSubstreamState::new(),
156 acpl_5x_pair_state: acpl_synth::Acpl5xPairPcmState::new(),
157 acpl_5x_mch_state: acpl_synth::Acpl5xMchPcmState::new(),
158 ssf_synth_state: Vec::new(),
159 ssf_walker_state: Vec::new(),
160 }
161 }
162
163 fn extract_raw_frame<'a>(&self, pkt: &'a Packet) -> (&'a [u8], bool) {
164 if let Some(f) = sync::find_sync_frame(&pkt.data) {
165 (f.payload, true)
166 } else {
167 (pkt.data.as_slice(), false)
168 }
169 }
170
171 /// Run the A-SPX bandwidth-extension pipeline on a block of
172 /// low-band PCM (produced by the core ASF/MDCT path) using the
173 /// derived A-SPX frequency tables: forward QMF, HF tile-copy via
174 /// the patch subband groups (§5.7.6.3.1.4 + §5.7.6.4.1.4
175 /// simplified), per-envelope HF envelope adjustment gains
176 /// (§5.7.6.4.2 Pseudocodes 90 / 91 / 95) when the substream
177 /// carried envelope deltas, noise + tone injection (§5.7.6.4.3 P102,
178 /// §5.7.6.4.4 P104, §5.7.6.4.5 P107/P108) driven by `add_harmonic`
179 /// flags + `scf_sig_sb` / `scf_noise_sb` (Pseudocode 92/94), and
180 /// otherwise a flat 0.5 gain scaffold. Finally runs inverse QMF
181 /// synthesis. Returns the bandwidth-extended PCM (f32) aligned to
182 /// the input PCM after accounting for the combined QMF group delay.
183 ///
184 /// `state` carries the noise/tone/sine-idx index state across calls
185 /// (one per decoder channel). `add_harmonic` is from the parsed
186 /// `aspx_hfgen_iwc_*` (Table 55/56) — empty/None if the substream
187 /// didn't carry it, in which case the tone generator stays silent
188 /// but noise still injects if envelope deltas are available.
189 /// `tna_mode` is `aspx_tna_mode[sbg]` from the same hfgen payload;
190 /// when present + FIXFIX framing, the HF generator runs the full
191 /// §5.7.6.4.1.3 chirp + α0 + α1 TNS body (Pseudocodes 86 → 89)
192 /// instead of the bare tile copy.
193 ///
194 /// If any preconditions fail (length not a multiple of 64, tables
195 /// missing, sbx >= 64) the original PCM is returned unchanged.
196 #[allow(clippy::too_many_arguments)]
197 fn aspx_extend_pcm(
198 pcm_in: &[f32],
199 tables: &aspx::AspxFrequencyTables,
200 cfg: &aspx::AspxConfig,
201 framing: Option<&aspx::AspxFraming>,
202 sig_deltas: Option<&[aspx::AspxHuffEnv]>,
203 noise_deltas: Option<&[aspx::AspxHuffEnv]>,
204 qmode_env: Option<aspx::AspxQuantStep>,
205 delta_dir: Option<&aspx::AspxDeltaDir>,
206 add_harmonic: Option<&[bool]>,
207 // §5.7.6.4.1.3 Pseudocode 88 — `aspx_tna_mode[sbg]` per noise
208 // subband group, drives chirp + α0 + α1 TNS path. `None` falls
209 // back to the bare HF tile copy.
210 tna_mode: Option<&[u8]>,
211 state: &mut aspx::AspxChannelExtState,
212 num_ts_in_ats: u32,
213 // Round 43: §5.7.5 companding tool — applied on the QMF matrix
214 // between envelope adjustment and QMF synthesis. `mode` selects
215 // the Pseudocode 121 sub-branch (`Off` / `PerSlot` / `Averaged`
216 // / `SyncPerSlot` / `SyncAveraged`). `sb0_override == Some(b)`
217 // overrides the lower band edge with `acpl_qmf_band` for the
218 // ASPX_ACPL_1 codec mode (per §5.7.5.2 sb0 selection); `None`
219 // falls back to `tables.sbx` (the A-SPX crossover, default
220 // for ASPX / SIMPLE).
221 compand_mode: aspx::CompandingMode,
222 compand_sb0_override: Option<u32>,
223 ) -> Vec<f32> {
224 let extended = Self::aspx_extend_to_qmf(
225 pcm_in,
226 tables,
227 cfg,
228 framing,
229 sig_deltas,
230 noise_deltas,
231 qmode_env,
232 delta_dir,
233 add_harmonic,
234 tna_mode,
235 state,
236 num_ts_in_ats,
237 );
238 match extended {
239 Some((mut q, sbx_eff, sbz_eff)) => {
240 let compand_sb0 = compand_sb0_override.unwrap_or(sbx_eff);
241 aspx::apply_companding_on_qmf_with_mode(&mut q, compand_sb0, sbz_eff, compand_mode);
242 Self::qmf_synthesise_pcm(&q, pcm_in.len())
243 }
244 None => pcm_in.to_vec(),
245 }
246 }
247
248 /// Round 44: phase-1 of the A-SPX HF-extension pipeline — runs the
249 /// QMF analysis, HF generation (TNS / tile-copy), envelope
250 /// adjustment + noise / tone injection, and updates `state` — but
251 /// stops BEFORE the §5.7.5 companding gain and the inverse-QMF
252 /// synthesis. Returns the post-extension QMF matrix `q[sb][ts]`
253 /// along with the (`sbx`, `sbz`) the companding tool will need.
254 ///
255 /// Returns `None` (and leaves `state` untouched in the same
256 /// preconditions [`aspx_extend_pcm`] historically returned the
257 /// input PCM verbatim) when the input fails the multiple-of-64
258 /// length check, the frequency tables are degenerate, or the
259 /// patches couldn't be derived.
260 ///
261 /// This split exists so that cross-channel synchronised companding
262 /// (`sync_flag == 1`, see [`aspx::apply_synchronised_companding_across_channels`])
263 /// can collect every channel's QMF matrix, compute the
264 /// geometric-mean gain across them, then apply the synced gain
265 /// uniformly before each channel runs its own synthesis via
266 /// [`Self::qmf_synthesise_pcm`].
267 #[allow(clippy::too_many_arguments)]
268 fn aspx_extend_to_qmf(
269 pcm_in: &[f32],
270 tables: &aspx::AspxFrequencyTables,
271 cfg: &aspx::AspxConfig,
272 framing: Option<&aspx::AspxFraming>,
273 sig_deltas: Option<&[aspx::AspxHuffEnv]>,
274 noise_deltas: Option<&[aspx::AspxHuffEnv]>,
275 qmode_env: Option<aspx::AspxQuantStep>,
276 delta_dir: Option<&aspx::AspxDeltaDir>,
277 add_harmonic: Option<&[bool]>,
278 tna_mode: Option<&[u8]>,
279 state: &mut aspx::AspxChannelExtState,
280 num_ts_in_ats: u32,
281 ) -> Option<AspxQmfPhase1> {
282 const NUM_QMF: usize = qmf::NUM_QMF_SUBBANDS;
283 // Need PCM length as a multiple of 64 for whole QMF slots.
284 if pcm_in.is_empty() || pcm_in.len() % NUM_QMF != 0 {
285 return None;
286 }
287 let sbx = tables.sbx as usize;
288 let sbz = tables.sbz as usize;
289 if sbx == 0 || sbx >= NUM_QMF || sbz <= sbx || sbz > NUM_QMF {
290 return None;
291 }
292 let n_slots = pcm_in.len() / NUM_QMF;
293 // Forward QMF analysis on the low-band PCM.
294 let mut ana = qmf::QmfAnalysisBank::new();
295 let slots = ana.process_block(pcm_in);
296 // Re-layout to q[sb][ts].
297 let mut q: Vec<Vec<(f32, f32)>> = (0..NUM_QMF)
298 .map(|_| vec![(0.0f32, 0.0f32); n_slots])
299 .collect();
300 for (ts, slot) in slots.iter().enumerate() {
301 for (sb, s) in slot.iter().enumerate() {
302 q[sb][ts] = *s;
303 }
304 }
305 // Derive patches from the master-freq-scale tables. 48 kHz
306 // family is the only base_samp_freq wired in the current
307 // TOC-driven pipeline; 44.1 kHz would pass `false` instead.
308 let is_highres = matches!(cfg.master_freq_scale, aspx::AspxMasterFreqScale::HighRes);
309 let patches = aspx::derive_patch_tables(
310 &tables.sbg_master,
311 tables.num_sbg_master,
312 tables.sba,
313 tables.sbx,
314 tables.num_sb_aspx,
315 true,
316 is_highres,
317 );
318 if patches.num_sbg_patches == 0 {
319 return None;
320 }
321 // Truncate the high band (ASPX substreams only carry spectral
322 // data up to sbx in the core path; the bandwidth-extension
323 // tool is responsible for filling sbx..sbz).
324 for row in q.iter_mut().skip(sbx) {
325 for sample in row.iter_mut() {
326 *sample = (0.0, 0.0);
327 }
328 }
329 // HF generation: when the substream gave us aspx_tna_mode + a
330 // framing we can derive atsg_sig and run the full §5.7.6.4.1.3
331 // / .4 TNS body (Pseudocodes 86 → 89). Covers FIXFIX (Pseudocode
332 // 76) and variable interval classes (Pseudocode 77).
333 // Otherwise fall back to the bare tile copy in §5.7.6.4.1.4.
334 let mut tns_used = false;
335 if let (Some(tna), Some(frm)) = (tna_mode, framing) {
336 let num_aspx_ts = (n_slots as u32) / num_ts_in_ats.max(1);
337 let atsg_sig_opt = aspx::derive_atsg_borders(num_aspx_ts, frm).map(|(s, _)| s);
338 if let Some(atsg_sig) = atsg_sig_opt {
339 if !tna.is_empty() {
340 let q_low_ext =
341 crate::aspx_tns::build_q_low_ext(&q, &state.q_low_prev, tables.sba);
342 let cov = crate::aspx_tns::compute_covariance(&q_low_ext, tables.sba);
343 let (alpha0, alpha1) = crate::aspx_tns::compute_alphas(&cov);
344 let chirp = crate::aspx_tns::chirp_factors(tna, &state.tns);
345 let gain_vec = if cfg.preflat {
346 Some(crate::aspx_tns::compute_preflat_gains(
347 &q,
348 tables.sbx,
349 &atsg_sig,
350 num_ts_in_ats,
351 ))
352 } else {
353 None
354 };
355 let q_high = crate::aspx_tns::hf_tile_tns(
356 &q_low_ext,
357 &patches,
358 &tables.sbg_noise,
359 &chirp.chirp_arr,
360 &alpha0,
361 &alpha1,
362 gain_vec.as_deref(),
363 tables.sbx,
364 NUM_QMF as u32,
365 &atsg_sig,
366 num_ts_in_ats,
367 );
368 for (dst, src) in q.iter_mut().zip(q_high.iter()).take(sbz).skip(sbx) {
369 let len = dst.len().min(src.len());
370 dst[..len].copy_from_slice(&src[..len]);
371 }
372 crate::aspx_tns::advance_tns_state(&mut state.tns, &chirp);
373 tns_used = true;
374 }
375 }
376 }
377 if !tns_used {
378 // Bare tile copy (§5.7.6.4.1.4 with chirp/α0/α1 = 0).
379 let q_high = aspx::hf_tile_copy(&q, &patches, tables.sbx, NUM_QMF as u32);
380 for (dst, src) in q.iter_mut().zip(q_high.iter()).take(sbz).skip(sbx) {
381 dst.clone_from(src);
382 }
383 }
384 // Snapshot Q_low for the next interval's Pseudocode 86 prefix.
385 // Only snapshot the actual low-band (sb < sba); the high-band
386 // is what we just synthesised, not part of Q_low.
387 state.q_low_prev = (0..(tables.sba as usize))
388 .map(|sb| {
389 if sb < q.len() {
390 q[sb].clone()
391 } else {
392 Vec::new()
393 }
394 })
395 .collect();
396 // Per-envelope HF envelope adjustment (§5.7.6.4.2 Pseudocodes
397 // 90 / 91 / 95) when the bitstream surface carried envelope
398 // deltas, followed by noise + tone injection (§5.7.6.4.3 / .4 /
399 // .5 Pseudocodes 102 / 104 / 107 / 108) when add_harmonic flags
400 // are available. Otherwise fall back to the flat-gain scaffold
401 // so output PCM still has audible HF content.
402 let mut used_envelope = false;
403 if let (Some(frm), Some(sig), Some(noise), Some(qm), Some(dd)) =
404 (framing, sig_deltas, noise_deltas, qmode_env, delta_dir)
405 {
406 let num_aspx_ts = (n_slots as u32) / num_ts_in_ats.max(1);
407 // §5.7.6.3.3.1 Pseudocode 76 (FIXFIX) or §5.7.6.3.3.2
408 // Pseudocode 77 (FIXVAR / VARFIX / VARVAR) border derivation.
409 if let Some((atsg_sig, atsg_noise)) = aspx::derive_atsg_borders(num_aspx_ts, frm) {
410 if sig.len() as u32 == frm.num_env {
411 let adjuster = aspx::AspxEnvelopeAdjuster::from_deltas(
412 &q,
413 tables,
414 sig,
415 noise,
416 qm,
417 &dd.sig_delta_dir,
418 &atsg_sig,
419 &atsg_noise,
420 num_ts_in_ats,
421 cfg.interpolation,
422 );
423 // Noise + tone injection on top of the
424 // envelope-adjusted HF. `add_harmonic` is sized
425 // to `num_sbg_sig_highres`; if the caller didn't
426 // provide one (no aspx_hfgen_iwc in the
427 // substream), default to an all-false slice so
428 // only the noise floor contributes.
429 let num_sbg_sig_highres = tables.sbg_sig_highres.len().saturating_sub(1);
430 let default_ah = vec![false; num_sbg_sig_highres];
431 let ah: &[bool] = match add_harmonic {
432 Some(s) if s.len() == num_sbg_sig_highres => s,
433 _ => &default_ah,
434 };
435 // tsg_ptr: 0 for FIXFIX (§4.3.10.4.7), from
436 // framing.tsg_ptr for variable interval classes.
437 let aspx_tsg_ptr: u32 = frm.tsg_ptr.map(|p| p as u32).unwrap_or(0);
438 if matches!(frm.int_class, aspx::AspxIntClass::FixFix) && cfg.limiter {
439 // §5.7.6.4.2.2 limiter pipeline (Pseudocodes
440 // 96 → 101) replaces the raw sig_gain with
441 // the boost-corrected sig_gain_sb_adj, so
442 // do NOT pre-apply adjuster.apply here.
443 aspx::inject_noise_and_tone_with_limiter(
444 &mut q,
445 &adjuster,
446 tables,
447 &patches,
448 &atsg_noise,
449 ah,
450 aspx_tsg_ptr,
451 state,
452 );
453 } else {
454 adjuster.apply(&mut q);
455 aspx::inject_noise_and_tone(
456 &mut q,
457 &adjuster,
458 tables,
459 &atsg_noise,
460 ah,
461 aspx_tsg_ptr,
462 state,
463 );
464 }
465 used_envelope = true;
466 }
467 }
468 }
469 if !used_envelope {
470 // Flat envelope gain fallback (scaffold kept for the
471 // non-FIXFIX / missing-envelope paths). Using 0.5 so the
472 // regenerated HF doesn't overwhelm the LF.
473 aspx::apply_flat_envelope_gain(&mut q, tables.sbx, tables.sbz, 0.5);
474 // Reset per-channel envelope/tone carry-over state — the
475 // envelope adjustment didn't run, so its index state has
476 // nothing consistent to advance. Next successful interval
477 // starts at master_reset semantics. The TNS chirp / α0 /
478 // α1 history (`state.tns` + `state.q_low_prev`) is
479 // independent and is *kept* — its update has already been
480 // recorded above when the TNS path ran.
481 state.noise.reset();
482 state.tone.reset();
483 state.sine_idx_sb_prev = None;
484 state.tsg_ptr_prev = 0;
485 state.num_atsg_sig_prev = 0;
486 }
487 // Phase-1 returns the post-extension QMF matrix + the (sbx,
488 // sbz) the §5.7.5 companding tool will need. Companding +
489 // inverse-QMF synthesis happen in `aspx_extend_pcm` (single
490 // channel) or in the caller via
491 // [`aspx::apply_synchronised_companding_across_channels`] +
492 // [`Self::qmf_synthesise_pcm`] (cross-channel sync_flag=1).
493 Some((q, tables.sbx, tables.sbz))
494 }
495
496 /// Round 44: phase-2 of the A-SPX HF-extension pipeline — runs the
497 /// inverse-QMF synthesis on a `q[sb][ts]` matrix and returns
498 /// `out_len`-long PCM. Caller is responsible for having applied
499 /// the §5.7.5 companding gain (per-channel via
500 /// [`aspx::apply_companding_on_qmf_with_mode`] or cross-channel
501 /// via [`aspx::apply_synchronised_companding_across_channels`]).
502 fn qmf_synthesise_pcm(q: &[Vec<(f32, f32)>], out_len: usize) -> Vec<f32> {
503 const NUM_QMF: usize = qmf::NUM_QMF_SUBBANDS;
504 if q.len() < NUM_QMF || out_len == 0 {
505 return Vec::new();
506 }
507 let n_slots = out_len / NUM_QMF;
508 let mut syn = qmf::QmfSynthesisBank::new();
509 let mut out = Vec::with_capacity(out_len);
510 #[allow(clippy::needless_range_loop)] // ETSI TS 103 190-2 §4.4.7 q[sb][ts] indexing
511 for ts in 0..n_slots {
512 let mut slot = [(0.0f32, 0.0f32); NUM_QMF];
513 for (sb, s) in slot.iter_mut().enumerate() {
514 *s = q[sb][ts];
515 }
516 let row = syn.process_slot(&slot);
517 out.extend_from_slice(&row);
518 }
519 out
520 }
521
522 /// Run IMDCT + KBD overlap-add for a single channel, returning
523 /// floating-point PCM (suitable for the A-SPX QMF pipeline).
524 fn imdct_channel_f32(&mut self, ch: usize, scaled: &[f32], n: usize) -> Vec<f32> {
525 // Transform-length change clears *all* channel overlap state so
526 // the next frame starts from a consistent history.
527 if self.prev_transform_length != n as u32 {
528 self.overlap.clear();
529 self.prev_transform_length = n as u32;
530 }
531 while self.overlap.len() <= ch {
532 self.overlap.push(vec![0.0_f32; n]);
533 }
534 if self.overlap[ch].len() != n {
535 self.overlap[ch] = vec![0.0_f32; n];
536 }
537 let mut x = vec![0.0_f32; n];
538 let copy = scaled.len().min(n);
539 x[..copy].copy_from_slice(&scaled[..copy]);
540 let y = mdct::imdct(&x);
541 let window = mdct::kbd_window(n as u32);
542 mdct::imdct_olap_symmetric(&y, &window, &mut self.overlap[ch])
543 }
544
545 /// Convert an f32 PCM buffer to i16, clamping to the i16 range.
546 fn pcm_f32_to_i16(pcm: &[f32]) -> Vec<i16> {
547 pcm.iter()
548 .map(|&s| (s * 32767.0).clamp(-32768.0, 32767.0) as i16)
549 .collect()
550 }
551
552 /// Run IMDCT + KBD overlap-add for a single channel. `ch` indexes
553 /// the per-channel overlap state (grown on demand). `scaled` is the
554 /// dequantised spectrum; bins past `scaled.len()` are zero-padded
555 /// up to N.
556 fn imdct_channel(&mut self, ch: usize, scaled: &[f32], n: usize) -> Vec<i16> {
557 let pcm_f = self.imdct_channel_f32(ch, scaled, n);
558 Self::pcm_f32_to_i16(&pcm_f)
559 }
560
561 /// SSF synthesis: drive §5.2.3-5.2.7 across every granule + block
562 /// in `data`, IMDCT each `n_mdct` block, overlap/add into the
563 /// channel's history, and emit a single
564 /// `frame_samples`-long S16 vector.
565 ///
566 /// Each SSF block produces an `n_mdct`-long spectrum; the IMDCT
567 /// then yields `2 * n_mdct` time-domain samples which the
568 /// overlap-add step combines with the previous block's tail to
569 /// emit `n_mdct` PCM samples. So one granule emits
570 /// `num_blocks * n_mdct = granule_length` samples; one frame's
571 /// `ssf_data` covers the entire frame_length.
572 fn run_ssf_channel(
573 &mut self,
574 ch: usize,
575 data: &crate::ssf::SsfData,
576 frame_samples: usize,
577 ) -> Vec<i16> {
578 // Drive the synth.
579 let state_idx = ch.min(self.ssf_synth_state.len().saturating_sub(1));
580 let mut spec_concat: Vec<f32> = Vec::new();
581 let mut block_lengths: Vec<usize> = Vec::new();
582 for granule in &data.granules {
583 let n_mdct = granule.n_mdct as usize;
584 if n_mdct == 0 {
585 continue;
586 }
587 // env_prev[] for SHORT_STRIDE P-frame interpolation now
588 // lives on `SsfSynthState` and the synth latches the
589 // resolved envelope at the end of each granule, so we pass
590 // an empty slice and let the synth pull the previous
591 // granule's envelope from `state.env_prev` (§5.2.3.0 Note 2).
592 let block =
593 ssf_synth::synthesize_granule(granule, &[], &mut self.ssf_synth_state[state_idx]);
594 // synthesize_granule returns num_blocks * n_mdct; track
595 // each block's n_mdct so the IMDCT loop can split them.
596 for _ in 0..(granule.num_blocks as usize) {
597 block_lengths.push(n_mdct);
598 }
599 spec_concat.extend_from_slice(&block);
600 }
601 if spec_concat.is_empty() || block_lengths.is_empty() {
602 return Vec::new();
603 }
604 // IMDCT each block independently and concat.
605 let mut pcm_out: Vec<f32> = Vec::with_capacity(frame_samples);
606 let mut off = 0usize;
607 for &n in &block_lengths {
608 if off + n > spec_concat.len() {
609 break;
610 }
611 let block_spec = &spec_concat[off..off + n];
612 // Use `imdct_channel_f32` for KBD-windowed overlap-add.
613 // SSF blocks share the channel's overlap state so the
614 // history chains across blocks within a frame.
615 let pcm_block = self.imdct_channel_f32(ch, block_spec, n);
616 pcm_out.extend_from_slice(&pcm_block);
617 off += n;
618 }
619 // Truncate / pad to frame_samples.
620 if pcm_out.len() > frame_samples {
621 pcm_out.truncate(frame_samples);
622 } else if pcm_out.len() < frame_samples {
623 pcm_out.resize(frame_samples, 0.0);
624 }
625 Self::pcm_f32_to_i16(&pcm_out)
626 }
627
628 /// IMDCT a `MonoLfeData` payload's `scaled_spec` to PCM `f32` using
629 /// the channel slot's overlap-add history. Returns `None` if the
630 /// mono shell didn't decode a body (LFE / SSF frontend / Huffman
631 /// miss) or if the carrier transform-length differs from `n`.
632 ///
633 /// `ch` is the per-channel overlap slot index (the centre channel
634 /// uses slot 2 for the 5.X path; surround Ls/Rs use 3/4 etc.).
635 fn imdct_mono_lfe_data_f32(
636 &mut self,
637 mono: &crate::mch::MonoLfeData,
638 ch: usize,
639 n: usize,
640 ) -> Option<Vec<f32>> {
641 let scaled = mono.scaled_spec.as_ref()?;
642 let ti = mono.transform_info.as_ref()?;
643 if ti.transform_length_0 as usize != n {
644 return None;
645 }
646 Some(self.imdct_channel_f32(ch, scaled, n))
647 }
648
649 /// §5.7.7.6.1 ASPX_ACPL_1 / ASPX_ACPL_2 5_X dispatch helper —
650 /// extracted from `receive_frame` so unit tests can drive it
651 /// without building a full 5_X TOC + body.
652 ///
653 /// Carries:
654 /// * `mode` — AspxAcpl1 (carrier-pair + Ls/Rs surround) or
655 /// AspxAcpl2 (carrier-pair only).
656 /// * `cfg` — single `acpl_config_1ch` shared between both
657 /// ACplModule's (per Pseudocode 117).
658 /// * `data_1` — `acpl_data_1ch_pair[0]` — L-side parameters.
659 /// * `data_2` — `acpl_data_1ch_pair[1]` — R-side parameters.
660 /// * `samples` — frame length in PCM samples.
661 /// * `centre_pcm` — optional centre channel PCM (already IMDCT +
662 /// overlap-added). When present and length-matched, used as the
663 /// `x2` carrier for Pseudocode 117's centre passthrough; when
664 /// `None`, falls back to silence (round-36 behaviour). Round 37
665 /// wires this from the parsed `cfg0_centre_mono.scaled_spec`.
666 /// * `ls_pcm` / `rs_pcm` — optional surround Ls/Rs carriers for
667 /// ASPX_ACPL_1 (Mode 1's `x3`/`x4` driving channels). When `None`
668 /// and `mode == AspxAcpl1`, falls back to silence (round-36
669 /// behaviour). Ignored entirely for `AspxAcpl2`.
670 /// * `pcm_per_channel` — slot list. Reads slots 0/1 as L/R carriers
671 /// (zero-fills if absent); writes slots 0..4 (L/R/C/Ls/Rs) on a
672 /// successful synthesis.
673 #[allow(clippy::too_many_arguments)]
674 fn dispatch_acpl_5x_pair(
675 &mut self,
676 mode: acpl_synth::Acpl5xPairMode,
677 cfg: &crate::acpl::AcplConfig1ch,
678 data_1: &crate::acpl::AcplData1ch,
679 data_2: &crate::acpl::AcplData1ch,
680 samples: usize,
681 centre_pcm: Option<&[f32]>,
682 ls_pcm: Option<&[f32]>,
683 rs_pcm: Option<&[f32]>,
684 pcm_per_channel: &mut Vec<Option<Vec<i16>>>,
685 ) {
686 let n = samples;
687 // run_acpl_5x_pair_pcm requires every PCM input to be a multiple
688 // of 64 (one QMF slot). Frame length in AC-4 is always a
689 // multiple of 64 by spec, but be defensive.
690 if n == 0 || n % qmf::NUM_QMF_SUBBANDS != 0 {
691 return;
692 }
693 let pcm_l_f32: Vec<f32> = pcm_per_channel
694 .first()
695 .and_then(|p| p.as_ref())
696 .map(|v| v.iter().map(|&s| s as f32 / 32767.0).collect())
697 .unwrap_or_else(|| vec![0.0_f32; n]);
698 let pcm_r_f32: Vec<f32> = pcm_per_channel
699 .get(1)
700 .and_then(|p| p.as_ref())
701 .map(|v| v.iter().map(|&s| s as f32 / 32767.0).collect())
702 .unwrap_or_else(|| vec![0.0_f32; n]);
703 // Centre carrier: real PCM if the caller supplied a length-matched
704 // buffer (round 37 wires this from the parsed centre mono data),
705 // else silence (round-36 placeholder behaviour).
706 let pcm_c_f32: Vec<f32> = match centre_pcm {
707 Some(p) if p.len() == n => p.to_vec(),
708 _ => vec![0.0_f32; n],
709 };
710 // Surround Ls/Rs carriers — only used in ACPL_1 mode. Real PCM
711 // when supplied + length-matched, else silence (round-36
712 // behaviour).
713 let pcm_ls_owned: Option<Vec<f32>> =
714 if matches!(mode, acpl_synth::Acpl5xPairMode::AspxAcpl1) {
715 Some(match ls_pcm {
716 Some(p) if p.len() == n => p.to_vec(),
717 _ => vec![0.0_f32; n],
718 })
719 } else {
720 None
721 };
722 let pcm_rs_owned: Option<Vec<f32>> =
723 if matches!(mode, acpl_synth::Acpl5xPairMode::AspxAcpl1) {
724 Some(match rs_pcm {
725 Some(p) if p.len() == n => p.to_vec(),
726 _ => vec![0.0_f32; n],
727 })
728 } else {
729 None
730 };
731 if let Some(out) = acpl_synth::run_acpl_5x_pair_pcm(
732 mode,
733 &pcm_l_f32,
734 &pcm_r_f32,
735 &pcm_c_f32,
736 pcm_ls_owned.as_deref(),
737 pcm_rs_owned.as_deref(),
738 cfg,
739 data_1,
740 cfg,
741 data_2,
742 &mut self.acpl_5x_pair_state,
743 ) {
744 // Output channel mapping for 5.0/5.1:
745 // ch0 = L, ch1 = R, ch2 = C, ch3 = Ls, ch4 = Rs.
746 while pcm_per_channel.len() < 5 {
747 pcm_per_channel.push(None);
748 }
749 pcm_per_channel[0] = Some(Self::pcm_f32_to_i16(&out.left));
750 pcm_per_channel[1] = Some(Self::pcm_f32_to_i16(&out.right));
751 pcm_per_channel[2] = Some(Self::pcm_f32_to_i16(&out.centre));
752 pcm_per_channel[3] = Some(Self::pcm_f32_to_i16(&out.left_surround));
753 pcm_per_channel[4] = Some(Self::pcm_f32_to_i16(&out.right_surround));
754 }
755 }
756
757 /// Apply A-SPX bandwidth-extension to one channel's IMDCT'd PCM
758 /// using a captured 5_X trailer slice. Wraps `aspx_extend_pcm` with
759 /// the trailer's per-channel envelopes / framing / hfgen state and
760 /// the trailer's frequency tables. `slot` indexes the per-channel
761 /// `aspx_ext_state` carry-over so each output slot keeps its own
762 /// noise / tone / TNS history.
763 #[allow(clippy::too_many_arguments)]
764 fn aspx_extend_with_trailer(
765 &mut self,
766 pcm_in: &[f32],
767 trailer: &aspx::FiveXAspxTrailer,
768 ch: &aspx::FiveXAspxChannelTrailer,
769 cfg: &aspx::AspxConfig,
770 slot: usize,
771 num_ts_in_ats: u32,
772 compand_mode: aspx::CompandingMode,
773 compand_sb0_override: Option<u32>,
774 ) -> Vec<f32> {
775 while self.aspx_ext_state.len() <= slot {
776 self.aspx_ext_state.push(aspx::AspxChannelExtState::new());
777 }
778 let state = &mut self.aspx_ext_state[slot];
779 Self::aspx_extend_pcm(
780 pcm_in,
781 &trailer.frequency_tables,
782 cfg,
783 Some(&ch.framing),
784 Some(&ch.data_sig),
785 Some(&ch.data_noise),
786 Some(ch.qmode_env),
787 Some(&ch.delta_dir),
788 ch.add_harmonic.as_deref(),
789 ch.tna_mode.as_deref(),
790 state,
791 num_ts_in_ats,
792 compand_mode,
793 compand_sb0_override,
794 )
795 }
796
797 /// Round 43: per-output-channel companding mode from the captured
798 /// `companding_control(num_chan)` for a 5_X frame. The Cfg2 / Cfg0 /
799 /// Cfg1 / Cfg3 paths all carry `companding_control(5)`, indexed by
800 /// the 5_X output channel `slot` (0..4 in L/R/C/Ls/Rs order). If
801 /// `sync_flag == true`, `compand_on[0]` applies to all five
802 /// channels (Table 116).
803 ///
804 /// Returns `CompandingMode::Off` whenever the parsed flags don't
805 /// reach the requested slot, otherwise resolves to one of the four
806 /// active sub-branches of Pseudocode 121
807 /// (`PerSlot` / `Averaged` / `SyncPerSlot` / `SyncAveraged`) per
808 /// [`aspx::CompandingMode::from_control`].
809 fn five_x_compand_mode_for_slot(
810 cc: Option<&aspx::CompandingControl>,
811 slot: usize,
812 ) -> aspx::CompandingMode {
813 match cc {
814 Some(cc) => aspx::CompandingMode::from_control(cc, slot),
815 None => aspx::CompandingMode::Off,
816 }
817 }
818
819 /// Backward-compat helper kept for round-42 unit tests — returns
820 /// the boolean "is companding active on this slot" derived from
821 /// the resolved [`aspx::CompandingMode`]. New code should call
822 /// [`Self::five_x_compand_mode_for_slot`] directly.
823 fn five_x_compand_on_for_slot(cc: Option<&aspx::CompandingControl>, slot: usize) -> bool {
824 !matches!(
825 Self::five_x_compand_mode_for_slot(cc, slot),
826 aspx::CompandingMode::Off
827 )
828 }
829
830 /// Round 44: cross-channel synchronised A-SPX bandwidth-extension
831 /// for the 5_X SIMPLE/ASPX path when the parsed
832 /// `companding_control()` carries `sync_flag == 1`.
833 ///
834 /// Pseudocode 121's `sync_flag == 1` branch defines the gain as
835 /// `g_synch(ts) = (∏_{ch=0..M} g_ch(ts))^(1/M)` and applies it
836 /// uniformly to every contributing channel — i.e. one cross-channel
837 /// gain per slot, NOT one per-channel gain. The pre-r44 pipeline
838 /// approximated this with the per-channel `g_ch(ts)` (exact for
839 /// `M = 1`); this entry-point closes the gap by:
840 ///
841 /// 1. Driving each contributing channel through phase-1
842 /// [`Self::aspx_extend_to_qmf`] to capture the post-extension
843 /// QMF matrix `q_ch[sb][ts]` along with each channel's
844 /// `(sb0, sbz)` companding band.
845 /// 2. Calling [`aspx::apply_synchronised_companding_across_channels`]
846 /// with the collected QMF matrices and bands — that walks
847 /// Pseudocode 121's geometric-mean across channels and writes
848 /// the synced gain back into every QMF matrix.
849 /// 3. Driving each channel through phase-2
850 /// [`Self::qmf_synthesise_pcm`] to produce the final PCM.
851 ///
852 /// Channels whose phase-1 returned `None` (length / table /
853 /// patch-derivation guard tripped — e.g. a slot whose IMDCT'd PCM
854 /// length isn't a multiple of 64) fall back to the unmodified
855 /// input PCM for that slot — same behaviour as the per-channel
856 /// `aspx_extend_pcm` helper used to give.
857 ///
858 /// `entries[i]` is `(slot, pcm_in, trailer, ch, sb0_override)`:
859 /// * `slot` — output channel index (0..=4 for 5_X), used to
860 /// pick the right `aspx_ext_state[slot]` carry-over.
861 /// * `pcm_in` — IMDCT'd LF PCM for that output channel.
862 /// * `trailer` — captured 5_X trailer (carries
863 /// `frequency_tables`).
864 /// * `ch` — primary or secondary channel within `trailer`.
865 /// * `sb0_override` — `Some(acpl_qmf_band)` for ASPX_ACPL_1
866 /// (`acpl_qmf_band` replaces `aspx_xover_band` per §5.7.5.2);
867 /// `None` for SIMPLE / ASPX (sb0 = trailer.sbx).
868 ///
869 /// Returns one `(slot, Vec<f32>)` per entry, in the order they
870 /// were passed in. The caller is responsible for the trailing
871 /// f32→i16 cast and writeback into `pcm_per_channel[slot]`.
872 ///
873 /// `mode` MUST be either [`aspx::CompandingMode::SyncPerSlot`] or
874 /// [`aspx::CompandingMode::SyncAveraged`]; no-op (i.e. the
875 /// per-channel pipeline outputs without companding gain) for any
876 /// other mode.
877 fn extend_5x_channels_with_sync_companding(
878 &mut self,
879 entries: &[SyncCompandingChannelEntry<'_>],
880 num_ts_in_ats: u32,
881 mode: aspx::CompandingMode,
882 ) -> Vec<(usize, Vec<f32>)> {
883 // Phase 1: drive each entry through aspx_extend_to_qmf,
884 // capturing the post-extension QMF matrix (or `None` if the
885 // extension preconditions tripped — that channel will pass
886 // through unchanged).
887 let mut phase1: Vec<(usize, usize, Option<AspxQmfPhase1>)> =
888 Vec::with_capacity(entries.len());
889 for (slot, pcm_in, trailer, ch, cfg, sb0_override) in entries.iter() {
890 while self.aspx_ext_state.len() <= *slot {
891 self.aspx_ext_state.push(aspx::AspxChannelExtState::new());
892 }
893 let state = &mut self.aspx_ext_state[*slot];
894 let qres = Self::aspx_extend_to_qmf(
895 pcm_in,
896 &trailer.frequency_tables,
897 cfg,
898 Some(&ch.framing),
899 Some(&ch.data_sig),
900 Some(&ch.data_noise),
901 Some(ch.qmode_env),
902 Some(&ch.delta_dir),
903 ch.add_harmonic.as_deref(),
904 ch.tna_mode.as_deref(),
905 state,
906 num_ts_in_ats,
907 );
908 // Resolve the effective sb0 for the synced companding —
909 // sb0_override (acpl_qmf_band for ASPX_ACPL_1) or sbx (the
910 // A-SPX crossover band for SIMPLE / ASPX).
911 let q_with_band = qres.map(|(q, sbx, sbz)| {
912 let sb0 = sb0_override.unwrap_or(sbx);
913 (q, sb0, sbz)
914 });
915 phase1.push((*slot, pcm_in.len(), q_with_band));
916 }
917 // Phase 2: collect every channel that survived phase 1 into
918 // the synced companding helper. Only mutable references to
919 // the QMF matrices are passed in; the helper reads each
920 // channel's level, computes geometric-mean across them, and
921 // writes back the synced scales.
922 {
923 let mut sync_view: Vec<aspx::SyncCompandingEntry<'_>> = Vec::new();
924 for (_, _, q_opt) in phase1.iter_mut() {
925 if let Some((q, sb0, sbz)) = q_opt.as_mut() {
926 sync_view.push((q, *sb0, *sbz));
927 }
928 }
929 aspx::apply_synchronised_companding_across_channels(&mut sync_view, mode);
930 }
931 // Phase 3: synthesise per-channel PCM. Channels whose phase
932 // returned None fall back to a clone of the input PCM (same
933 // contract as the original `aspx_extend_pcm`).
934 let mut out: Vec<(usize, Vec<f32>)> = Vec::with_capacity(entries.len());
935 for (i, (slot, pcm_len, q_opt)) in phase1.into_iter().enumerate() {
936 let pcm = match q_opt {
937 Some((q, _, _)) => Self::qmf_synthesise_pcm(&q, pcm_len),
938 None => entries[i].1.to_vec(),
939 };
940 out.push((slot, pcm));
941 }
942 out
943 }
944
945 /// Round 45: stereo-CPE counterpart to
946 /// [`Self::extend_5x_channels_with_sync_companding`] for the M=2
947 /// case where the two channels are not 5_X trailer slots but the
948 /// primary / secondary of an `aspx_data_2ch` stereo CPE — in
949 /// particular the L/R carrier pair that drives a 5_X ASPX_ACPL_3
950 /// `run_acpl_5x_mch_pcm` synthesis (Pseudocode 118 expects the
951 /// extended carriers, not raw IMDCT'd PCM).
952 ///
953 /// When `companding_control(2)` carried `sync_flag == 1` the spec's
954 /// `g_synch(ts) = (∏_{ch=0..M} g_ch(ts))^(1/M)` collapses for M=2
955 /// to `√(g_0(ts) · g_1(ts))` — a single geometric-mean gain shared
956 /// across both channels rather than two independent per-channel
957 /// gains. This matches r44's 5_X SIMPLE/ASPX dispatch path:
958 /// phase-1 runs each channel through [`Self::aspx_extend_to_qmf`]
959 /// (capturing the post-extension QMF matrix + each channel's
960 /// `(sb0, sbz)` companding band), phase-2 calls
961 /// [`aspx::apply_synchronised_companding_across_channels`] to
962 /// write the synced gain into both QMF matrices, and phase-3
963 /// runs each channel through [`Self::qmf_synthesise_pcm`] to
964 /// produce the final PCM.
965 ///
966 /// `mode` MUST be one of
967 /// [`aspx::CompandingMode::SyncPerSlot`] / [`aspx::CompandingMode::SyncAveraged`]
968 /// (the cross-channel sync sub-branches of Pseudocode 121); any
969 /// other mode is a no-op for the synced pipeline and the caller
970 /// should run the per-channel `aspx_extend_pcm` path instead.
971 ///
972 /// `tables` / `cfg` are shared between the two channels in a
973 /// stereo CPE (one `aspx_config()` per substream). `sb0_override`
974 /// is `Some(acpl_qmf_band)` for the stereo ASPX_ACPL_1 path
975 /// (which substitutes `acpl_qmf_band` for `aspx_xover_band` per
976 /// §5.7.5.2 sb0 selection); `None` everywhere else (SIMPLE / ASPX
977 /// / ACPL_3 paths use `tables.sbx`).
978 ///
979 /// When either channel's phase-1 returns `None` (PCM length not a
980 /// multiple of 64, missing tables, etc.) that channel falls back
981 /// to its un-extended PCM — same contract as
982 /// [`Self::aspx_extend_pcm`] / [`Self::extend_5x_channels_with_sync_companding`].
983 #[allow(clippy::too_many_arguments)]
984 fn extend_stereo_cpe_pair_with_sync_companding(
985 &mut self,
986 primary: &StereoCpeChannelInput<'_>,
987 secondary: &StereoCpeChannelInput<'_>,
988 tables: &aspx::AspxFrequencyTables,
989 cfg: &aspx::AspxConfig,
990 num_ts_in_ats: u32,
991 mode: aspx::CompandingMode,
992 sb0_override: Option<u32>,
993 ) -> (Vec<f32>, Vec<f32>) {
994 // Phase 1: drive each channel through aspx_extend_to_qmf and
995 // capture the post-extension QMF matrix + (sb0, sbz) band.
996 // Lay out as Vec so indices are stable across the
997 // borrow-juggle below.
998 let mut phase1: [(usize, usize, Option<AspxQmfPhase1>); 2] = [
999 (primary.ch_index, primary.pcm_in.len(), None),
1000 (secondary.ch_index, secondary.pcm_in.len(), None),
1001 ];
1002 for (i, input) in [primary, secondary].iter().enumerate() {
1003 while self.aspx_ext_state.len() <= input.ch_index {
1004 self.aspx_ext_state.push(aspx::AspxChannelExtState::new());
1005 }
1006 let state = &mut self.aspx_ext_state[input.ch_index];
1007 phase1[i].2 = Self::aspx_extend_to_qmf(
1008 input.pcm_in,
1009 tables,
1010 cfg,
1011 input.framing,
1012 input.sig,
1013 input.noise,
1014 input.qmode,
1015 input.delta_dir,
1016 input.add_harmonic,
1017 input.tna_mode,
1018 state,
1019 num_ts_in_ats,
1020 )
1021 .map(|(q, sbx_eff, sbz_eff)| {
1022 // sb0_override is shared across both channels of the
1023 // stereo CPE (acpl_qmf_band for ASPX_ACPL_1 stereo,
1024 // sbx everywhere else).
1025 let sb0 = sb0_override.unwrap_or(sbx_eff);
1026 (q, sb0, sbz_eff)
1027 });
1028 }
1029 // Phase 2: collect every channel that survived phase 1 into
1030 // the synced companding helper. M=2 → `g_synch(ts) = √(g_0(ts) · g_1(ts))`
1031 // is written back into BOTH QMF matrices uniformly.
1032 {
1033 let mut sync_view: Vec<aspx::SyncCompandingEntry<'_>> = Vec::with_capacity(2);
1034 for (_, _, q_opt) in phase1.iter_mut() {
1035 if let Some((q, sb0, sbz)) = q_opt.as_mut() {
1036 sync_view.push((q, *sb0, *sbz));
1037 }
1038 }
1039 aspx::apply_synchronised_companding_across_channels(&mut sync_view, mode);
1040 }
1041 // Phase 3: synthesise per-channel PCM. Channels whose phase-1
1042 // returned None fall back to the unmodified input PCM (same
1043 // contract as `aspx_extend_pcm`).
1044 let pcm_out = |idx: usize, fallback: &[f32]| -> Vec<f32> {
1045 match &phase1[idx].2 {
1046 Some((q, _, _)) => Self::qmf_synthesise_pcm(q, phase1[idx].1),
1047 None => fallback.to_vec(),
1048 }
1049 };
1050 let pri = pcm_out(0, primary.pcm_in);
1051 let sec = pcm_out(1, secondary.pcm_in);
1052 (pri, sec)
1053 }
1054
1055 /// Round 44: shared front-end for the 5_X SIMPLE/ASPX dispatchers
1056 /// that resolves the synced-companding mode for the whole 5_X
1057 /// frame. With `sync_flag == 1`, every channel resolves to the
1058 /// SAME mode (Pseudocode 121 broadcasts `compand_on[0]`); with
1059 /// `sync_flag == 0` (or no companding) the per-channel
1060 /// [`Self::five_x_compand_mode_for_slot`] is what callers want.
1061 ///
1062 /// Returns `Some(mode)` when the cross-channel synced pipeline
1063 /// should run (mode is `SyncPerSlot` or `SyncAveraged`); `None`
1064 /// when the per-channel pipeline should run (sync_flag missing /
1065 /// false, or sync_flag=true resolves to `Off`).
1066 fn five_x_synced_mode(cc: Option<&aspx::CompandingControl>) -> Option<aspx::CompandingMode> {
1067 let cc = cc?;
1068 if !matches!(cc.sync_flag, Some(true)) {
1069 return None;
1070 }
1071 let mode = aspx::CompandingMode::from_control(cc, 0);
1072 match mode {
1073 aspx::CompandingMode::SyncPerSlot | aspx::CompandingMode::SyncAveraged => Some(mode),
1074 _ => None,
1075 }
1076 }
1077
1078 /// Round 44: drive every entry through the synced-companding
1079 /// pipeline (when `synced_mode` is `Some`), apply the resulting
1080 /// PCM to `pcm_per_channel[slot]`. Otherwise (sync mode = None),
1081 /// drive each entry through the per-channel pipeline.
1082 ///
1083 /// Each entry is `(slot, pcm_f, trailer, ch, sb0_override)`.
1084 /// `aspx_cfg` is shared across all entries (one config per 5_X
1085 /// substream).
1086 #[allow(clippy::too_many_arguments)]
1087 fn extend_5x_entries(
1088 &mut self,
1089 entries: Vec<FiveXChannelEntry<'_>>,
1090 aspx_cfg: Option<aspx::AspxConfig>,
1091 companding: Option<&aspx::CompandingControl>,
1092 num_ts_in_ats: u32,
1093 pcm_per_channel: &mut [Option<Vec<i16>>],
1094 ) {
1095 let synced = Self::five_x_synced_mode(companding);
1096 if let (Some(mode), Some(cfg)) = (synced, aspx_cfg) {
1097 // Cross-channel synced path. Build the entries-with-trailer
1098 // list (skipping any whose trailer is missing — those fall
1099 // back to the unmodified PCM for that slot).
1100 let mut sync_entries: Vec<SyncCompandingChannelEntry<'_>> = Vec::new();
1101 // Track which entries had no trailer — they pass through
1102 // the PCM unchanged.
1103 let mut passthrough: Vec<(usize, &[f32])> = Vec::new();
1104 for (slot, pcm_f, trailer_pair) in entries.iter() {
1105 match trailer_pair {
1106 Some((trailer, is_secondary)) => {
1107 let ch = if *is_secondary {
1108 trailer.secondary.as_ref().unwrap_or(&trailer.primary)
1109 } else {
1110 &trailer.primary
1111 };
1112 sync_entries.push((*slot, pcm_f.as_slice(), trailer, ch, &cfg, None));
1113 }
1114 None => {
1115 passthrough.push((*slot, pcm_f.as_slice()));
1116 }
1117 }
1118 }
1119 let extended =
1120 self.extend_5x_channels_with_sync_companding(&sync_entries, num_ts_in_ats, mode);
1121 for (slot, pcm) in extended {
1122 pcm_per_channel[slot] = Some(Self::pcm_f32_to_i16(&pcm));
1123 }
1124 for (slot, pcm) in passthrough {
1125 pcm_per_channel[slot] = Some(Self::pcm_f32_to_i16(pcm));
1126 }
1127 return;
1128 }
1129 // Per-channel path (sync_flag == 0 or sync_flag == 1 + Off).
1130 for (slot, pcm_f, trailer_pair) in entries.into_iter() {
1131 let pcm_i16 = match (aspx_cfg, trailer_pair) {
1132 (Some(cfg), Some((trailer, is_secondary))) => {
1133 let ch = if is_secondary {
1134 trailer.secondary.as_ref().unwrap_or(&trailer.primary)
1135 } else {
1136 &trailer.primary
1137 };
1138 let compand_mode = Self::five_x_compand_mode_for_slot(companding, slot);
1139 let extended = self.aspx_extend_with_trailer(
1140 &pcm_f,
1141 trailer,
1142 ch,
1143 &cfg,
1144 slot,
1145 num_ts_in_ats,
1146 compand_mode,
1147 None,
1148 );
1149 Self::pcm_f32_to_i16(&extended)
1150 }
1151 _ => Self::pcm_f32_to_i16(&pcm_f),
1152 };
1153 pcm_per_channel[slot] = Some(pcm_i16);
1154 }
1155 }
1156
1157 /// §5.3.4.3.1 / Table 180 — 5_X SIMPLE/ASPX `coding_config == 2`
1158 /// dispatch: the parsed `four_channel_data` carries L/R/Ls/Rs in
1159 /// `scaled_spec_per_channel[0..4]` and the trailing
1160 /// `cfg2_back_mono.scaled_spec` carries the centre. Channel mapping
1161 /// per Table 180:
1162 ///
1163 /// ```text
1164 /// four_channel_data[0] -> slot 0 (L)
1165 /// four_channel_data[1] -> slot 1 (R)
1166 /// four_channel_data[2] -> slot 3 (Ls)
1167 /// four_channel_data[3] -> slot 4 (Rs)
1168 /// mono_data -> slot 2 (C)
1169 /// ```
1170 ///
1171 /// Round 41 wires the ASPX bandwidth-extension trailer per channel:
1172 /// `aspx_data_2ch[L,R] + aspx_data_2ch[Ls,Rs] + aspx_data_1ch[C]`
1173 /// (per Table 25 row `case ASPX:`). Each trailer's per-channel
1174 /// envelope set drives `aspx_extend_pcm` on the IMDCT'd low-band
1175 /// PCM before quantisation. When a trailer is absent (SIMPLE mode
1176 /// or trailer-parse miss) the channel passes through with low-band
1177 /// PCM only — matching the round-38 behaviour for those paths.
1178 ///
1179 /// The function is a no-op when any of the four per-channel scaled
1180 /// spectra are absent (short / grouped frame or Huffman miss);
1181 /// centre is silent when the trailing `mono_data` body is absent.
1182 #[allow(clippy::too_many_arguments)]
1183 fn dispatch_5x_cfg2_simple_aspx(
1184 &mut self,
1185 four: &crate::mch::FourChannelData,
1186 back_mono: Option<&crate::mch::MonoLfeData>,
1187 aspx_lr: Option<&aspx::FiveXAspxTrailer>,
1188 aspx_ls_rs: Option<&aspx::FiveXAspxTrailer>,
1189 aspx_centre: Option<&aspx::FiveXAspxTrailer>,
1190 aspx_cfg: Option<aspx::AspxConfig>,
1191 companding: Option<&aspx::CompandingControl>,
1192 num_ts_in_ats: u32,
1193 samples: usize,
1194 pcm_per_channel: &mut Vec<Option<Vec<i16>>>,
1195 ) {
1196 let Some(ti) = four.transform_info.as_ref() else {
1197 return;
1198 };
1199 let n = ti.transform_length_0 as usize;
1200 if n == 0 || n != samples {
1201 return;
1202 }
1203 if four.scaled_spec_per_channel.len() < 4 {
1204 return;
1205 }
1206 // Channel mapping: ch_in -> slot_out per Table 180 cfg2 column.
1207 // The ASPX trailers map (L,R) and (Ls,Rs) onto the front /
1208 // surround stereo pairs.
1209 const SLOT_MAP: [usize; 4] = [0, 1, 3, 4];
1210 // Need at least 5 output slots (L/R/C/Ls/Rs). Resize on demand.
1211 while pcm_per_channel.len() < 5 {
1212 pcm_per_channel.push(None);
1213 }
1214 // L (slot 0) — primary channel of the L/R 2ch trailer.
1215 // R (slot 1) — secondary channel of the L/R 2ch trailer.
1216 // Ls (slot 3) — primary channel of the Ls/Rs 2ch trailer.
1217 // Rs (slot 4) — secondary channel of the Ls/Rs 2ch trailer.
1218 let trailers_for_ch: [Option<(&aspx::FiveXAspxTrailer, bool)>; 4] = [
1219 aspx_lr.map(|t| (t, false)), // L
1220 aspx_lr.map(|t| (t, true)), // R
1221 aspx_ls_rs.map(|t| (t, false)), // Ls
1222 aspx_ls_rs.map(|t| (t, true)), // Rs
1223 ];
1224 // Build the per-slot entries (slot, pcm_f, trailer_pair) for
1225 // the L/R/Ls/Rs quartet, plus the centre. The centre joins the
1226 // synced-companding cohort when both `back_mono` and a centre
1227 // trailer are present — that way Pseudocode 121's
1228 // `g_synch(ts) = (∏ g_ch(ts))^(1/M)` averages across all five
1229 // 5_X channels, not just the four front/surround.
1230 let mut entries: Vec<FiveXChannelEntry<'_>> = Vec::with_capacity(5);
1231 for (ch_in, &slot) in SLOT_MAP.iter().enumerate() {
1232 let Some(scaled) = four.scaled_spec_per_channel[ch_in].as_ref() else {
1233 continue;
1234 };
1235 let pcm_f = self.imdct_channel_f32(slot, scaled, n);
1236 entries.push((slot, pcm_f, trailers_for_ch[ch_in]));
1237 }
1238 if let Some(mono) = back_mono {
1239 if let Some(pcm_f) = self.imdct_mono_lfe_data_f32(mono, 2, samples) {
1240 let centre_pair = aspx_centre.map(|t| (t, false));
1241 entries.push((2, pcm_f, centre_pair));
1242 }
1243 }
1244 self.extend_5x_entries(
1245 entries,
1246 aspx_cfg,
1247 companding,
1248 num_ts_in_ats,
1249 pcm_per_channel,
1250 );
1251 }
1252
1253 /// §5.3.4.3.1 / Table 180 — 5_X SIMPLE/ASPX `coding_config == 0`
1254 /// dispatch. The body shape is
1255 /// `b_2ch_mode + two_channel_data + two_channel_data + mono_data(0)`,
1256 /// with channel mapping driven by the 1-bit `b_2ch_mode`:
1257 ///
1258 /// ```text
1259 /// 2ch_mode == 0 (Table 180 column 0a):
1260 /// two_channel_data[0] -> [0, 1] (L, R)
1261 /// two_channel_data[1] -> [3, 4] (Ls, Rs)
1262 /// mono_data -> [2] (C)
1263 ///
1264 /// 2ch_mode == 1 (Table 180 column 0b):
1265 /// two_channel_data[0] -> [0, 3] (L, Ls)
1266 /// two_channel_data[1] -> [1, 4] (R, Rs)
1267 /// mono_data -> [2] (C)
1268 /// ```
1269 ///
1270 /// The function is a no-op when `tcd_a` doesn't carry a transform_info
1271 /// matching `samples`, when fewer than two `two_channel_data` shells
1272 /// are present, or when any per-channel scaled spectrum is missing
1273 /// (short / grouped / Huffman-miss path). Centre is silent when the
1274 /// trailing `mono_data` body is absent.
1275 #[allow(clippy::too_many_arguments)]
1276 fn dispatch_5x_cfg0_simple_aspx(
1277 &mut self,
1278 tcd_a: &crate::mch::TwoChannelData,
1279 tcd_b: &crate::mch::TwoChannelData,
1280 b_2ch_mode: bool,
1281 centre_mono: Option<&crate::mch::MonoLfeData>,
1282 aspx_lr: Option<&aspx::FiveXAspxTrailer>,
1283 aspx_ls_rs: Option<&aspx::FiveXAspxTrailer>,
1284 aspx_centre: Option<&aspx::FiveXAspxTrailer>,
1285 aspx_cfg: Option<aspx::AspxConfig>,
1286 companding: Option<&aspx::CompandingControl>,
1287 num_ts_in_ats: u32,
1288 samples: usize,
1289 pcm_per_channel: &mut Vec<Option<Vec<i16>>>,
1290 ) {
1291 let Some(ti_a) = tcd_a.transform_info.as_ref() else {
1292 return;
1293 };
1294 let n_a = ti_a.transform_length_0 as usize;
1295 if n_a == 0 || n_a != samples {
1296 return;
1297 }
1298 let Some(ti_b) = tcd_b.transform_info.as_ref() else {
1299 return;
1300 };
1301 let n_b = ti_b.transform_length_0 as usize;
1302 if n_b == 0 || n_b != samples {
1303 return;
1304 }
1305 if tcd_a.scaled_spec_per_channel.len() < 2 || tcd_b.scaled_spec_per_channel.len() < 2 {
1306 return;
1307 }
1308 // Slot map per Table 180 column 0:
1309 // 2ch_mode == 0: [0,1] then [3,4] (L,R / Ls,Rs)
1310 // 2ch_mode == 1: [0,3] then [1,4] (L,Ls / R,Rs)
1311 let slot_map_a: [usize; 2] = if b_2ch_mode { [0, 3] } else { [0, 1] };
1312 let slot_map_b: [usize; 2] = if b_2ch_mode { [1, 4] } else { [3, 4] };
1313 while pcm_per_channel.len() < 5 {
1314 pcm_per_channel.push(None);
1315 }
1316 // Trailer-to-output-slot mapping (independent of b_2ch_mode):
1317 // ASPX is applied per output channel after channel-element
1318 // decode produces PCM. Per Table 25 trailer order:
1319 // slot 0 (L) -> aspx_lr.primary
1320 // slot 1 (R) -> aspx_lr.secondary
1321 // slot 3 (Ls) -> aspx_ls_rs.primary
1322 // slot 4 (Rs) -> aspx_ls_rs.secondary
1323 // slot 2 (C) -> aspx_centre.primary
1324 let mut entries: Vec<FiveXChannelEntry<'_>> = Vec::with_capacity(5);
1325 for (ch_in, &slot) in slot_map_a.iter().enumerate() {
1326 let Some(scaled) = tcd_a.scaled_spec_per_channel[ch_in].as_ref() else {
1327 continue;
1328 };
1329 let pcm_f = self.imdct_channel_f32(slot, scaled, n_a);
1330 entries.push((
1331 slot,
1332 pcm_f,
1333 Self::trailer_for_5x_slot(slot, aspx_lr, aspx_ls_rs, aspx_centre),
1334 ));
1335 }
1336 for (ch_in, &slot) in slot_map_b.iter().enumerate() {
1337 let Some(scaled) = tcd_b.scaled_spec_per_channel[ch_in].as_ref() else {
1338 continue;
1339 };
1340 let pcm_f = self.imdct_channel_f32(slot, scaled, n_b);
1341 entries.push((
1342 slot,
1343 pcm_f,
1344 Self::trailer_for_5x_slot(slot, aspx_lr, aspx_ls_rs, aspx_centre),
1345 ));
1346 }
1347 if let Some(mono) = centre_mono {
1348 if let Some(pcm_f) = self.imdct_mono_lfe_data_f32(mono, 2, samples) {
1349 entries.push((
1350 2,
1351 pcm_f,
1352 Self::trailer_for_5x_slot(2, aspx_lr, aspx_ls_rs, aspx_centre),
1353 ));
1354 }
1355 }
1356 self.extend_5x_entries(
1357 entries,
1358 aspx_cfg,
1359 companding,
1360 num_ts_in_ats,
1361 pcm_per_channel,
1362 );
1363 }
1364
1365 /// Round 42: canonical Table-25 trailer-to-slot mapping for the
1366 /// 5_X SIMPLE/ASPX dispatchers. Returns `(trailer, is_secondary)`
1367 /// when the appropriate trailer is present, else `None`.
1368 fn trailer_for_5x_slot<'a>(
1369 slot: usize,
1370 aspx_lr: Option<&'a aspx::FiveXAspxTrailer>,
1371 aspx_ls_rs: Option<&'a aspx::FiveXAspxTrailer>,
1372 aspx_centre: Option<&'a aspx::FiveXAspxTrailer>,
1373 ) -> Option<(&'a aspx::FiveXAspxTrailer, bool)> {
1374 match slot {
1375 0 => aspx_lr.map(|t| (t, false)),
1376 1 => aspx_lr.map(|t| (t, true)),
1377 2 => aspx_centre.map(|t| (t, false)),
1378 3 => aspx_ls_rs.map(|t| (t, false)),
1379 4 => aspx_ls_rs.map(|t| (t, true)),
1380 _ => None,
1381 }
1382 }
1383
1384 /// Round 42: trailer-aware ASPX extension on one 5_X output slot
1385 /// (0..=4). Used by `dispatch_5x_cfg{0,1,3}_simple_aspx` to apply
1386 /// the per-channel trailer + companding pulled from the per-cfg
1387 /// slots in [`crate::asf::SubstreamTools`].
1388 ///
1389 /// Trailer-to-slot mapping is the canonical Table-25 order
1390 /// `aspx_data_2ch + aspx_data_2ch + aspx_data_1ch` translated to
1391 /// 5.X output channels:
1392 /// slot 0 (L) -> aspx_lr.primary
1393 /// slot 1 (R) -> aspx_lr.secondary
1394 /// slot 3 (Ls) -> aspx_ls_rs.primary
1395 /// slot 4 (Rs) -> aspx_ls_rs.secondary
1396 /// slot 2 (C) -> aspx_centre.primary
1397 /// Trailers / config absent -> i16 cast of `pcm_f` only.
1398 #[allow(dead_code)]
1399 #[allow(clippy::too_many_arguments)]
1400 fn maybe_extend_5x_slot(
1401 &mut self,
1402 slot: usize,
1403 pcm_f: Vec<f32>,
1404 aspx_lr: Option<&aspx::FiveXAspxTrailer>,
1405 aspx_ls_rs: Option<&aspx::FiveXAspxTrailer>,
1406 aspx_centre: Option<&aspx::FiveXAspxTrailer>,
1407 aspx_cfg: Option<aspx::AspxConfig>,
1408 companding: Option<&aspx::CompandingControl>,
1409 num_ts_in_ats: u32,
1410 ) -> Vec<i16> {
1411 let trailer_pair = Self::trailer_for_5x_slot(slot, aspx_lr, aspx_ls_rs, aspx_centre);
1412 match (aspx_cfg, trailer_pair) {
1413 (Some(cfg), Some((trailer, is_secondary))) => {
1414 let ch = if is_secondary {
1415 trailer.secondary.as_ref().unwrap_or(&trailer.primary)
1416 } else {
1417 &trailer.primary
1418 };
1419 let compand_mode = Self::five_x_compand_mode_for_slot(companding, slot);
1420 let extended = self.aspx_extend_with_trailer(
1421 &pcm_f,
1422 trailer,
1423 ch,
1424 &cfg,
1425 slot,
1426 num_ts_in_ats,
1427 compand_mode,
1428 // SIMPLE/ASPX cfg{0,1,3} dispatchers never run on
1429 // ASPX_ACPL_1, so sb0 stays at aspx_xover_band.
1430 None,
1431 );
1432 Self::pcm_f32_to_i16(&extended)
1433 }
1434 _ => Self::pcm_f32_to_i16(&pcm_f),
1435 }
1436 }
1437
1438 /// §5.3.4.3.1 / Table 180 — 5_X SIMPLE/ASPX `coding_config == 1`
1439 /// dispatch. The body shape is
1440 /// `three_channel_data + two_channel_data`, with channel mapping per
1441 /// Table 180 column 1:
1442 ///
1443 /// ```text
1444 /// three_channel_data[0..3] -> [0, 1, 2] (L, R, C)
1445 /// two_channel_data[0..2] -> [3, 4] (Ls, Rs)
1446 /// ```
1447 ///
1448 /// No-op on transform-length / sample-count mismatch, or when a
1449 /// per-channel scaled spectrum is absent.
1450 #[allow(clippy::too_many_arguments)]
1451 fn dispatch_5x_cfg1_simple_aspx(
1452 &mut self,
1453 three: &crate::mch::ThreeChannelData,
1454 tcd: &crate::mch::TwoChannelData,
1455 aspx_lr: Option<&aspx::FiveXAspxTrailer>,
1456 aspx_ls_rs: Option<&aspx::FiveXAspxTrailer>,
1457 aspx_centre: Option<&aspx::FiveXAspxTrailer>,
1458 aspx_cfg: Option<aspx::AspxConfig>,
1459 companding: Option<&aspx::CompandingControl>,
1460 num_ts_in_ats: u32,
1461 samples: usize,
1462 pcm_per_channel: &mut Vec<Option<Vec<i16>>>,
1463 ) {
1464 let Some(ti3) = three.transform_info.as_ref() else {
1465 return;
1466 };
1467 let n3 = ti3.transform_length_0 as usize;
1468 if n3 == 0 || n3 != samples {
1469 return;
1470 }
1471 let Some(ti2) = tcd.transform_info.as_ref() else {
1472 return;
1473 };
1474 let n2 = ti2.transform_length_0 as usize;
1475 if n2 == 0 || n2 != samples {
1476 return;
1477 }
1478 if three.scaled_spec_per_channel.len() < 3 || tcd.scaled_spec_per_channel.len() < 2 {
1479 return;
1480 }
1481 while pcm_per_channel.len() < 5 {
1482 pcm_per_channel.push(None);
1483 }
1484 const THREE_SLOTS: [usize; 3] = [0, 1, 2];
1485 let mut entries: Vec<FiveXChannelEntry<'_>> = Vec::with_capacity(5);
1486 for (ch_in, &slot) in THREE_SLOTS.iter().enumerate() {
1487 let Some(scaled) = three.scaled_spec_per_channel[ch_in].as_ref() else {
1488 continue;
1489 };
1490 let pcm_f = self.imdct_channel_f32(slot, scaled, n3);
1491 entries.push((
1492 slot,
1493 pcm_f,
1494 Self::trailer_for_5x_slot(slot, aspx_lr, aspx_ls_rs, aspx_centre),
1495 ));
1496 }
1497 const TWO_SLOTS: [usize; 2] = [3, 4];
1498 for (ch_in, &slot) in TWO_SLOTS.iter().enumerate() {
1499 let Some(scaled) = tcd.scaled_spec_per_channel[ch_in].as_ref() else {
1500 continue;
1501 };
1502 let pcm_f = self.imdct_channel_f32(slot, scaled, n2);
1503 entries.push((
1504 slot,
1505 pcm_f,
1506 Self::trailer_for_5x_slot(slot, aspx_lr, aspx_ls_rs, aspx_centre),
1507 ));
1508 }
1509 self.extend_5x_entries(
1510 entries,
1511 aspx_cfg,
1512 companding,
1513 num_ts_in_ats,
1514 pcm_per_channel,
1515 );
1516 }
1517
1518 /// §5.3.4.3.1 / Table 180 — 5_X SIMPLE/ASPX `coding_config == 3`
1519 /// dispatch. The body is a single `five_channel_data`; channel
1520 /// mapping is the identity:
1521 ///
1522 /// ```text
1523 /// five_channel_data[0..5] -> [0, 1, 2, 3, 4] (L, R, C, Ls, Rs)
1524 /// ```
1525 ///
1526 /// No-op on transform-length / sample-count mismatch, or when a
1527 /// per-channel scaled spectrum is absent.
1528 #[allow(clippy::too_many_arguments)]
1529 fn dispatch_5x_cfg3_simple_aspx(
1530 &mut self,
1531 five: &crate::mch::FiveChannelData,
1532 aspx_lr: Option<&aspx::FiveXAspxTrailer>,
1533 aspx_ls_rs: Option<&aspx::FiveXAspxTrailer>,
1534 aspx_centre: Option<&aspx::FiveXAspxTrailer>,
1535 aspx_cfg: Option<aspx::AspxConfig>,
1536 companding: Option<&aspx::CompandingControl>,
1537 num_ts_in_ats: u32,
1538 samples: usize,
1539 pcm_per_channel: &mut Vec<Option<Vec<i16>>>,
1540 ) {
1541 let Some(ti) = five.transform_info.as_ref() else {
1542 return;
1543 };
1544 let n = ti.transform_length_0 as usize;
1545 if n == 0 || n != samples {
1546 return;
1547 }
1548 if five.scaled_spec_per_channel.len() < 5 {
1549 return;
1550 }
1551 while pcm_per_channel.len() < 5 {
1552 pcm_per_channel.push(None);
1553 }
1554 const SLOT_MAP: [usize; 5] = [0, 1, 2, 3, 4];
1555 let mut entries: Vec<FiveXChannelEntry<'_>> = Vec::with_capacity(5);
1556 for (ch_in, &slot) in SLOT_MAP.iter().enumerate() {
1557 let Some(scaled) = five.scaled_spec_per_channel[ch_in].as_ref() else {
1558 continue;
1559 };
1560 let pcm_f = self.imdct_channel_f32(slot, scaled, n);
1561 entries.push((
1562 slot,
1563 pcm_f,
1564 Self::trailer_for_5x_slot(slot, aspx_lr, aspx_ls_rs, aspx_centre),
1565 ));
1566 }
1567 self.extend_5x_entries(
1568 entries,
1569 aspx_cfg,
1570 companding,
1571 num_ts_in_ats,
1572 pcm_per_channel,
1573 );
1574 }
1575
1576 /// §5.3.4.4.1 / Table 182 / Table 183 — 7_X SIMPLE/ASPX additional-
1577 /// channel pair dispatch. The `seven_x_additional_channel_data` shell
1578 /// carries two `sf_data(ASF)` bodies for the F / G preliminary output
1579 /// channels (Table 182). The optional `partner_pair_spectra` carry the
1580 /// 5.X-core counterparts D/E (or A/B per `channel_mode`) that pair
1581 /// with F/G in the Table 183 SAP matrix.
1582 ///
1583 /// With `b_use_sap_add_ch == false` (or absent), Table 183's SAP
1584 /// matrix collapses to identity — F / G land directly on slots 5 / 6
1585 /// and the partner spectra are untouched (their independent IMDCT
1586 /// path produces the unmodified slots 3 / 4 elsewhere in the
1587 /// pipeline).
1588 ///
1589 /// With `b_use_sap_add_ch == true`, the per-sfb (a, b, c, d)
1590 /// coefficients are extracted from each `chparam_info` (Pseudocode 59
1591 /// via [`crate::asf::extract_sap_abcd`]) and applied to the spectral
1592 /// pair (P, F) → (slot_partner, slot_F+1) and (Q, G) → (slot_partner+1,
1593 /// slot_F+2) per the Table 183 row for the active channel_mode:
1594 ///
1595 /// ```text
1596 /// [out_high] [a b] [partner]
1597 /// [ ] = [ ] · [ ]
1598 /// [out_low ] [c d] [add_ch ]
1599 /// ```
1600 ///
1601 /// where `out_high` lands on the partner's existing slot (overwriting
1602 /// the unmixed PCM at that slot) and `out_low` lands on the
1603 /// additional-pair slot. When partner spectra are absent or the
1604 /// transform lengths don't match, falls back to identity render
1605 /// (slots 5 / 6 from F / G unmodified).
1606 ///
1607 /// No-op on transform-length / sample-count mismatch, or when either
1608 /// per-channel scaled spectrum is absent (short / grouped frame /
1609 /// Huffman miss).
1610 fn dispatch_7x_additional_channel_pair(
1611 &mut self,
1612 add: &crate::mch::TwoChannelData,
1613 partner_pair_spectra: Option<[&[f32]; 2]>,
1614 partner_slots: [usize; 2],
1615 chparam: Option<&[asf::ChparamInfo; 2]>,
1616 samples: usize,
1617 pcm_per_channel: &mut Vec<Option<Vec<i16>>>,
1618 ) {
1619 let Some(ti) = add.transform_info.as_ref() else {
1620 return;
1621 };
1622 let n = ti.transform_length_0 as usize;
1623 if n == 0 || n != samples {
1624 return;
1625 }
1626 if add.scaled_spec_per_channel.len() < 2 {
1627 return;
1628 }
1629 // Slots 5 / 6 are the additional-pair output channels (F, G).
1630 let pair_out_slots: [usize; 2] = [5, 6];
1631 while pcm_per_channel.len() < 7 {
1632 pcm_per_channel.push(None);
1633 }
1634 // Resize so partner slots are addressable too.
1635 for &slot in partner_slots.iter() {
1636 while pcm_per_channel.len() <= slot {
1637 pcm_per_channel.push(None);
1638 }
1639 }
1640 // Per-pair SAP application: for each i in 0..2, mix
1641 // `partner_pair_spectra[i]` (P or Q) with `add[i]` (F or G).
1642 // When partner is absent or chparam is None, falls through to
1643 // identity (only the additional-pair F/G is rendered).
1644 let tl = ti.transform_length_0;
1645 let max_sfb_cap = crate::tables::num_sfb_48(tl).unwrap_or(0);
1646 for ch_in in 0..2 {
1647 let Some(scaled_add) = add.scaled_spec_per_channel[ch_in].as_ref() else {
1648 continue;
1649 };
1650 // Build SAP coefficients for this pair if requested.
1651 let abcd: Option<Vec<(f32, f32, f32, f32)>> = match (chparam, partner_pair_spectra) {
1652 (Some(cps), Some(partners))
1653 if max_sfb_cap > 0 && partners[ch_in].len() == n && scaled_add.len() == n =>
1654 {
1655 let coeffs = asf::extract_sap_abcd(&cps[ch_in], &[max_sfb_cap]);
1656 coeffs.abcd.into_iter().next()
1657 }
1658 _ => None,
1659 };
1660 if let (Some(abcd_row), Some(partners)) = (abcd.as_ref(), partner_pair_spectra) {
1661 // Spectral SAP per-sfb. Mix (P, F) -> (out_high, out_low).
1662 let partner = partners[ch_in];
1663 let sfbo = match crate::sfb_offset::sfb_offset_48(tl) {
1664 Some(s) => s,
1665 None => {
1666 // SFB table missing — fall through to identity.
1667 let pcm = self.imdct_channel(pair_out_slots[ch_in], scaled_add, n);
1668 pcm_per_channel[pair_out_slots[ch_in]] = Some(pcm);
1669 continue;
1670 }
1671 };
1672 let mut out_high = vec![0.0f32; n];
1673 let mut out_low = vec![0.0f32; n];
1674 let usable_sfb = abcd_row.len().min(max_sfb_cap as usize);
1675 for sfb in 0..usable_sfb {
1676 let lo = sfbo[sfb] as usize;
1677 let hi = sfbo[sfb + 1] as usize;
1678 let hi = hi.min(n).min(partner.len()).min(scaled_add.len());
1679 let (a, b, c, d) = abcd_row[sfb];
1680 for k in lo..hi {
1681 let p = partner[k];
1682 let f = scaled_add[k];
1683 out_high[k] = a * p + b * f;
1684 out_low[k] = c * p + d * f;
1685 }
1686 }
1687 // Copy untouched bands (sfb >= usable_sfb) from the
1688 // partner / add spectra so the high half retains
1689 // the partner's bandwidth and the low half is silent
1690 // outside the SAP-coded range.
1691 let unmixed_start = sfbo
1692 .get(usable_sfb)
1693 .copied()
1694 .map(|v| v as usize)
1695 .unwrap_or(n);
1696 let unmixed_lo = unmixed_start.min(n);
1697 let unmixed_hi = n.min(partner.len());
1698 if unmixed_lo < unmixed_hi {
1699 out_high[unmixed_lo..unmixed_hi]
1700 .copy_from_slice(&partner[unmixed_lo..unmixed_hi]);
1701 }
1702 let pcm_high = self.imdct_channel(partner_slots[ch_in], &out_high, n);
1703 pcm_per_channel[partner_slots[ch_in]] = Some(pcm_high);
1704 let pcm_low = self.imdct_channel(pair_out_slots[ch_in], &out_low, n);
1705 pcm_per_channel[pair_out_slots[ch_in]] = Some(pcm_low);
1706 } else {
1707 // Identity passthrough — only render the additional pair
1708 // (slots 5/6). Partner slots untouched (their independent
1709 // 5_X-core IMDCT runs separately).
1710 let pcm = self.imdct_channel(pair_out_slots[ch_in], scaled_add, n);
1711 pcm_per_channel[pair_out_slots[ch_in]] = Some(pcm);
1712 }
1713 }
1714 }
1715}
1716
1717impl Decoder for Ac4Decoder {
1718 fn codec_id(&self) -> &CodecId {
1719 &self.codec_id
1720 }
1721
1722 fn send_packet(&mut self, packet: &Packet) -> Result<()> {
1723 if self.pending.is_some() {
1724 return Err(Error::other(
1725 "ac4 decoder: call receive_frame before sending another packet",
1726 ));
1727 }
1728 self.pending = Some(packet.clone());
1729 Ok(())
1730 }
1731
1732 fn receive_frame(&mut self) -> Result<Frame> {
1733 let Some(pkt) = self.pending.take() else {
1734 return if self.eof {
1735 Err(Error::Eof)
1736 } else {
1737 Err(Error::NeedMore)
1738 };
1739 };
1740 if pkt.data.is_empty() {
1741 // Empty packet — emit a 0-sample frame so the pipeline
1742 // continues rather than erroring.
1743 return Ok(Frame::Audio(AudioFrame {
1744 samples: 0,
1745 pts: pkt.pts,
1746 data: vec![Vec::new()],
1747 }));
1748 }
1749 let (raw, _had_sync) = self.extract_raw_frame(&pkt);
1750 let info = toc::parse_ac4_toc(raw)
1751 .map_err(|e| Error::invalid(format!("ac4 decoder: TOC parse failed: {e}")))?;
1752 // Resolve shape with fallbacks to the container hint when the
1753 // TOC carried a reserved / escape value.
1754 let channels = if info.channels == 0 {
1755 self.hint_channels
1756 } else {
1757 info.channels
1758 };
1759 let sample_rate = if info.sample_rate == 0 {
1760 self.hint_sample_rate
1761 } else {
1762 info.sample_rate
1763 };
1764 let samples = if info.frame_length == 0 {
1765 // Unknown frame length (reserved frame_rate_index): fall back
1766 // to 1024 samples at 48 kHz, 480 @ 44.1 kHz — both
1767 // round-numbers the resampler handles cleanly.
1768 if sample_rate == 44_100 {
1769 480
1770 } else {
1771 1024
1772 }
1773 } else {
1774 // frame_length in the table is expressed at the base sample
1775 // rate; for 96/192 kHz (sf_multiplier) we scale up.
1776 if sample_rate == 96_000 {
1777 info.frame_length * 2
1778 } else if sample_rate == 192_000 {
1779 info.frame_length * 4
1780 } else {
1781 info.frame_length
1782 }
1783 };
1784 // Best-effort walk of the first substream. The exact byte offset
1785 // of substream 0 is `toc_len + payload_base`, where `toc_len` is
1786 // the length of the byte-aligned ac4_toc() element. We don't
1787 // currently track `toc_len` out of [`toc::parse_ac4_toc`]; as a
1788 // cheap approximation we try the first substream size if the
1789 // substream_index_table exposed one, carving the tail of the
1790 // packet. This is fine for single-substream frames (the
1791 // overwhelmingly common case).
1792 let substream_try = {
1793 // Substream 0 starts at toc_size + payload_base.
1794 let start = (info.toc_size + info.payload_base) as usize;
1795 let first_size = info.substream_sizes.first().copied();
1796 if start >= raw.len() {
1797 None
1798 } else if let Some(sz) = first_size {
1799 let sz = sz as usize;
1800 let end = start.saturating_add(sz).min(raw.len());
1801 if sz > 0 {
1802 Some(&raw[start..end])
1803 } else {
1804 None
1805 }
1806 } else {
1807 // Single-substream frame with implicit size: the
1808 // substream spans to the end of the packet (possibly
1809 // minus CRC bytes, which the syncframe layer stripped).
1810 Some(&raw[start..])
1811 }
1812 };
1813 // Round 32: grow the per-channel SSF walker state vector to
1814 // match the current frame's channel count *before* invoking the
1815 // walker so the state borrow has the right shape.
1816 while self.ssf_walker_state.len() < channels as usize {
1817 self.ssf_walker_state.push(ssf::SsfChannelState::new());
1818 }
1819 self.last_substream = substream_try.and_then(|sb| {
1820 let channels_u16 = channels;
1821 let b_iframe = info
1822 .presentations
1823 .first()
1824 .map(|p| p.b_iframe)
1825 .unwrap_or(info.b_iframe_global);
1826 asf::walk_ac4_substream_stateful(
1827 sb,
1828 channels_u16,
1829 b_iframe,
1830 info.frame_length,
1831 Some(&mut self.ssf_walker_state[..channels as usize]),
1832 )
1833 .ok()
1834 });
1835 // If we have scaled spectra for the substream, run IMDCT + OLA
1836 // and produce real PCM. Per-channel PCM buffers live in
1837 // `pcm_per_channel`; the interleaver below lays them out to the
1838 // frame's channel count. Any channel without decoded spectra
1839 // stays silent. We detach the per-channel inputs from
1840 // `last_substream` up front so the IMDCT step can mutate
1841 // `self.overlap` without a borrow conflict.
1842 let mut pcm_per_channel: Vec<Option<Vec<i16>>> = vec![None; channels as usize];
1843 // Detach the inputs + the ASPX tables once so we can run IMDCT
1844 // (which mutates overlap state) and the ASPX extension without
1845 // a borrow conflict on self.
1846 // Detach A-CPL config + parsed data so the synth call below
1847 // doesn't conflict with the immutable borrow of `last_substream`
1848 // when we later mutate decoder state.
1849 let acpl_active_cfg = self.last_substream.as_ref().and_then(|sub| {
1850 sub.tools
1851 .acpl_config_1ch_full
1852 .or(sub.tools.acpl_config_1ch_partial)
1853 });
1854 let acpl_active_data = self
1855 .last_substream
1856 .as_ref()
1857 .and_then(|sub| sub.tools.acpl_data_1ch.clone());
1858 // Detach SSF data so we can run §5.2.3-5.2.7 synthesis without
1859 // a borrow conflict on `self`. SSF substreams are mutually
1860 // exclusive with ASF on a per-channel basis (per
1861 // `spec_frontend`), so when these are populated the IMDCT input
1862 // for that channel comes from `synthesize_ssf_data` instead of
1863 // the ASF Huffman path.
1864 let ssf_primary = self
1865 .last_substream
1866 .as_ref()
1867 .and_then(|sub| sub.tools.ssf_data_primary.clone());
1868 let ssf_secondary = self
1869 .last_substream
1870 .as_ref()
1871 .and_then(|sub| sub.tools.ssf_data_secondary.clone());
1872 // Detach 5_X ASPX_ACPL_3 synthesis inputs: two carrier spectra
1873 // land on scaled_spec_primary / scaled_spec_secondary (via the
1874 // stereo body walker), centre from cfg0_centre_mono, and the
1875 // A-CPL parameter pair from acpl_config_2ch / acpl_data_2ch.
1876 // Only populated when five_x_mode == AspxAcpl3.
1877 let five_x_acpl3_active = self
1878 .last_substream
1879 .as_ref()
1880 .map(|sub| {
1881 matches!(
1882 sub.tools.five_x_mode,
1883 Some(crate::mch::FiveXCodecMode::AspxAcpl3)
1884 ) && sub.tools.acpl_config_2ch.is_some()
1885 && sub.tools.acpl_data_2ch.is_some()
1886 && sub.tools.scaled_spec_primary.is_some()
1887 && sub.tools.scaled_spec_secondary.is_some()
1888 })
1889 .unwrap_or(false);
1890 let five_x_acpl3_cfg = self
1891 .last_substream
1892 .as_ref()
1893 .and_then(|sub| sub.tools.acpl_config_2ch);
1894 let five_x_acpl3_data = self
1895 .last_substream
1896 .as_ref()
1897 .and_then(|sub| sub.tools.acpl_data_2ch.clone());
1898 // Detach 5_X ASPX_ACPL_1 / ASPX_ACPL_2 synthesis inputs
1899 // (Pseudocode 117). The active acpl_config_1ch is one of:
1900 // - acpl_config_1ch_partial (ASPX_ACPL_1 — surround Ls/Rs
1901 // carriers come from extra mono carriers; here we silence
1902 // them as placeholders since the standalone Ls/Rs decode
1903 // path isn't fleshed out yet).
1904 // - acpl_config_1ch_full (ASPX_ACPL_2 — no surround carriers).
1905 // The two `acpl_data_1ch_pair[]` entries drive the L-side
1906 // (alpha_1/beta_1) and R-side (alpha_2/beta_2) ACplModule's.
1907 let five_x_pair_mode: Option<acpl_synth::Acpl5xPairMode> = self
1908 .last_substream
1909 .as_ref()
1910 .and_then(|sub| match sub.tools.five_x_mode {
1911 Some(crate::mch::FiveXCodecMode::AspxAcpl1) => {
1912 Some(acpl_synth::Acpl5xPairMode::AspxAcpl1)
1913 }
1914 Some(crate::mch::FiveXCodecMode::AspxAcpl2) => {
1915 Some(acpl_synth::Acpl5xPairMode::AspxAcpl2)
1916 }
1917 _ => None,
1918 });
1919 let five_x_pair_cfg =
1920 self.last_substream
1921 .as_ref()
1922 .and_then(|sub| match sub.tools.five_x_mode {
1923 Some(crate::mch::FiveXCodecMode::AspxAcpl1) => {
1924 sub.tools.acpl_config_1ch_partial
1925 }
1926 Some(crate::mch::FiveXCodecMode::AspxAcpl2) => sub.tools.acpl_config_1ch_full,
1927 _ => None,
1928 });
1929 let five_x_pair_data_1 = self
1930 .last_substream
1931 .as_ref()
1932 .and_then(|sub| sub.tools.acpl_data_1ch_pair[0].clone());
1933 let five_x_pair_data_2 = self
1934 .last_substream
1935 .as_ref()
1936 .and_then(|sub| sub.tools.acpl_data_1ch_pair[1].clone());
1937 let five_x_pair_active = five_x_pair_mode.is_some()
1938 && five_x_pair_cfg.is_some()
1939 && five_x_pair_data_1.is_some()
1940 && five_x_pair_data_2.is_some();
1941 // Round 37: detach the parsed `cfg0_centre_mono` payload (Cfg0
1942 // trailing `mono_data(0)`) for the 5_X pair / 7_X pair paths so
1943 // we can IMDCT its `scaled_spec` into a real centre carrier
1944 // (replacing the silence-placeholder used in round 36). For
1945 // ACPL_3 the centre is also pulled from the same source. The
1946 // detach is a clone so the substream tools borrow can be
1947 // released before we mutate decoder IMDCT state.
1948 let cfg0_centre_mono = self
1949 .last_substream
1950 .as_ref()
1951 .and_then(|sub| sub.tools.cfg0_centre_mono.clone());
1952 // Round 38 / 39: detach the 5_X SIMPLE/ASPX `coding_config`
1953 // payloads so we can drive end-to-end multichannel decode.
1954 // Round 38 wired Cfg2 (four_channel_data + cfg2_back_mono);
1955 // round 39 adds Cfg0 (b_2ch_mode + 2x two_channel_data +
1956 // cfg0_centre_mono), Cfg1 (three_channel_data + two_channel_data),
1957 // and Cfg3 (five_channel_data). Each helper computes its own
1958 // gating; we just detach the inputs once.
1959 let five_x_simple_aspx_active = self
1960 .last_substream
1961 .as_ref()
1962 .map(|sub| {
1963 matches!(
1964 sub.tools.five_x_mode,
1965 Some(crate::mch::FiveXCodecMode::Simple)
1966 | Some(crate::mch::FiveXCodecMode::Aspx)
1967 )
1968 })
1969 .unwrap_or(false);
1970 let five_x_coding_cfg = self
1971 .last_substream
1972 .as_ref()
1973 .and_then(|sub| sub.tools.five_x_coding_config);
1974 let cfg2_four_channel_data = self
1975 .last_substream
1976 .as_ref()
1977 .and_then(|sub| sub.tools.four_channel_data.clone());
1978 let cfg2_back_mono = self
1979 .last_substream
1980 .as_ref()
1981 .and_then(|sub| sub.tools.cfg2_back_mono.clone());
1982 // Round 41: 5_X SIMPLE/ASPX cfg2 ASPX trailer detach. The
1983 // outer walker populates these when `5_X_codec_mode == ASPX`
1984 // (the SIMPLE path leaves them None and the dispatch falls
1985 // back to low-band only PCM).
1986 let cfg2_aspx_lr = self
1987 .last_substream
1988 .as_ref()
1989 .and_then(|sub| sub.tools.cfg2_aspx_lr.clone());
1990 let cfg2_aspx_ls_rs = self
1991 .last_substream
1992 .as_ref()
1993 .and_then(|sub| sub.tools.cfg2_aspx_ls_rs.clone());
1994 let cfg2_aspx_centre = self
1995 .last_substream
1996 .as_ref()
1997 .and_then(|sub| sub.tools.cfg2_aspx_centre.clone());
1998 // Round 42: cfg0 / cfg1 / cfg3 ASPX trailer detach.
1999 let cfg0_aspx_lr = self
2000 .last_substream
2001 .as_ref()
2002 .and_then(|sub| sub.tools.cfg0_aspx_lr.clone());
2003 let cfg0_aspx_ls_rs = self
2004 .last_substream
2005 .as_ref()
2006 .and_then(|sub| sub.tools.cfg0_aspx_ls_rs.clone());
2007 let cfg0_aspx_centre = self
2008 .last_substream
2009 .as_ref()
2010 .and_then(|sub| sub.tools.cfg0_aspx_centre.clone());
2011 let cfg1_aspx_lr = self
2012 .last_substream
2013 .as_ref()
2014 .and_then(|sub| sub.tools.cfg1_aspx_lr.clone());
2015 let cfg1_aspx_ls_rs = self
2016 .last_substream
2017 .as_ref()
2018 .and_then(|sub| sub.tools.cfg1_aspx_ls_rs.clone());
2019 let cfg1_aspx_centre = self
2020 .last_substream
2021 .as_ref()
2022 .and_then(|sub| sub.tools.cfg1_aspx_centre.clone());
2023 let cfg3_aspx_lr = self
2024 .last_substream
2025 .as_ref()
2026 .and_then(|sub| sub.tools.cfg3_aspx_lr.clone());
2027 let cfg3_aspx_ls_rs = self
2028 .last_substream
2029 .as_ref()
2030 .and_then(|sub| sub.tools.cfg3_aspx_ls_rs.clone());
2031 let cfg3_aspx_centre = self
2032 .last_substream
2033 .as_ref()
2034 .and_then(|sub| sub.tools.cfg3_aspx_centre.clone());
2035 let five_x_aspx_config = self
2036 .last_substream
2037 .as_ref()
2038 .and_then(|sub| sub.tools.aspx_config);
2039 // Round 42: companding_control() per-channel flags. The 5_X
2040 // ASPX path captures companding(3) (L/R, Ls/Rs, C) into
2041 // `tools.companding`; we lift the parsed flags here so the
2042 // dispatch can hand a per-channel companding-on bool to the
2043 // `aspx_extend_with_trailer` wrapper.
2044 let five_x_companding = self
2045 .last_substream
2046 .as_ref()
2047 .and_then(|sub| sub.tools.companding.clone());
2048 // Cfg0 / Cfg1 / Cfg3 5_X SIMPLE/ASPX detach. Round 39: the walker
2049 // already populates the same `tools.three_channel_data` /
2050 // `four_channel_data` / `five_channel_data` / `two_channel_data`
2051 // slots; here we detach clones for the dispatch helpers.
2052 let cfg_two_channel_data: Vec<crate::mch::TwoChannelData> = self
2053 .last_substream
2054 .as_ref()
2055 .map(|sub| sub.tools.two_channel_data.clone())
2056 .unwrap_or_default();
2057 let cfg_b_2ch_mode = self
2058 .last_substream
2059 .as_ref()
2060 .and_then(|sub| sub.tools.b_2ch_mode);
2061 let cfg_three_channel_data = self
2062 .last_substream
2063 .as_ref()
2064 .and_then(|sub| sub.tools.three_channel_data.clone());
2065 let cfg_five_channel_data = self
2066 .last_substream
2067 .as_ref()
2068 .and_then(|sub| sub.tools.five_channel_data.clone());
2069 // Round 39: 7_X SIMPLE/ASPX additional-channel pair (Table 182).
2070 // The walker populates `seven_x_additional_channel_data` with two
2071 // `sf_data(ASF)` bodies for the F / G preliminary outputs (slots
2072 // 5 / 6 in the bitstream order). Render with identity SAP for now.
2073 let seven_x_additional_channel_data = self
2074 .last_substream
2075 .as_ref()
2076 .and_then(|sub| sub.tools.seven_x_additional_channel_data.clone());
2077 let seven_x_simple_aspx_active = self
2078 .last_substream
2079 .as_ref()
2080 .map(|sub| {
2081 matches!(
2082 sub.tools.seven_x_mode,
2083 Some(crate::mch::SevenXCodecMode::Simple)
2084 | Some(crate::mch::SevenXCodecMode::Aspx)
2085 )
2086 })
2087 .unwrap_or(false);
2088 // Round 37: 7_X ASPX_ACPL_1 / ASPX_ACPL_2 pair dispatch state
2089 // (mirrors the 5_X detach above). Both modes carry the same
2090 // shape of `acpl_config_1ch_*` + `acpl_data_1ch_pair`. The 7_X
2091 // walker also fires for 7.0 and 7.1 (b_has_lfe). Channel
2092 // mapping per Table 202 — for ACPL_1/_2 (no SIMPLE/ASPX
2093 // additional-channel block in scope), z6/z7 stay silent and
2094 // we populate slots 0..4 (L/R/C/Ls/Rs) only.
2095 let seven_x_pair_mode: Option<acpl_synth::Acpl5xPairMode> = self
2096 .last_substream
2097 .as_ref()
2098 .and_then(|sub| match sub.tools.seven_x_mode {
2099 Some(crate::mch::SevenXCodecMode::AspxAcpl1) => {
2100 Some(acpl_synth::Acpl5xPairMode::AspxAcpl1)
2101 }
2102 Some(crate::mch::SevenXCodecMode::AspxAcpl2) => {
2103 Some(acpl_synth::Acpl5xPairMode::AspxAcpl2)
2104 }
2105 _ => None,
2106 });
2107 let seven_x_pair_cfg =
2108 self.last_substream
2109 .as_ref()
2110 .and_then(|sub| match sub.tools.seven_x_mode {
2111 Some(crate::mch::SevenXCodecMode::AspxAcpl1) => {
2112 sub.tools.acpl_config_1ch_partial
2113 }
2114 Some(crate::mch::SevenXCodecMode::AspxAcpl2) => sub.tools.acpl_config_1ch_full,
2115 _ => None,
2116 });
2117 let seven_x_pair_data_1 = self
2118 .last_substream
2119 .as_ref()
2120 .and_then(|sub| sub.tools.acpl_data_1ch_pair[0].clone());
2121 let seven_x_pair_data_2 = self
2122 .last_substream
2123 .as_ref()
2124 .and_then(|sub| sub.tools.acpl_data_1ch_pair[1].clone());
2125 let seven_x_pair_active = seven_x_pair_mode.is_some()
2126 && seven_x_pair_cfg.is_some()
2127 && seven_x_pair_data_1.is_some()
2128 && seven_x_pair_data_2.is_some();
2129 // Centre channel for ASPX_ACPL_3: round 38 wires the parsed
2130 // `cfg0_centre_mono.scaled_spec` (when present) through IMDCT +
2131 // overlap-add for slot 2 (centre). This replaces the round-37
2132 // silence placeholder used while the body decoder was deferred.
2133 // Falls back to a zero-filled placeholder when the centre body
2134 // isn't decoded (LFE / SSF / Huffman miss / ACPL_3 walker
2135 // doesn't populate cfg0_centre_mono on every frame) so the
2136 // length-checked run_acpl_5x_mch_pcm still fires and emits
2137 // shaped Ls/Rs from the L/R carriers.
2138 let five_x_centre_spec: Option<Vec<f32>> = if five_x_acpl3_active {
2139 let centre_pcm = cfg0_centre_mono
2140 .as_ref()
2141 .and_then(|m| self.imdct_mono_lfe_data_f32(m, 2, samples as usize));
2142 Some(centre_pcm.unwrap_or_else(|| vec![0.0_f32; samples as usize]))
2143 } else {
2144 None
2145 };
2146 // ASPX_ACPL_1 (joint-MDCT residual layer): M spectrum lives on
2147 // `scaled_spec_primary`, S on `scaled_spec_secondary`; both
2148 // share the same transform_info. Detect it via the parsed
2149 // stereo_codec_mode + acpl_config_1ch_partial (`partial` is the
2150 // ACPL_1 flavour).
2151 let acpl1_active = self
2152 .last_substream
2153 .as_ref()
2154 .map(|sub| {
2155 matches!(sub.tools.stereo_mode, Some(asf::StereoCodecMode::AspxAcpl1))
2156 && sub.tools.acpl_config_1ch_partial.is_some()
2157 && sub.tools.scaled_spec_primary.is_some()
2158 && sub.tools.scaled_spec_secondary.is_some()
2159 })
2160 .unwrap_or(false);
2161 let (
2162 primary_in,
2163 secondary_in,
2164 aspx_tables,
2165 aspx_cfg,
2166 framing_pri,
2167 framing_sec,
2168 sig_pri,
2169 sig_sec,
2170 noise_pri,
2171 noise_sec,
2172 qmode_pri,
2173 qmode_sec,
2174 delta_dir_pri,
2175 delta_dir_sec,
2176 ah_pri,
2177 ah_sec,
2178 tna_pri,
2179 tna_sec,
2180 ) = if let Some(sub) = self.last_substream.as_ref() {
2181 let pri = sub
2182 .tools
2183 .scaled_spec_primary
2184 .as_ref()
2185 .zip(sub.tools.transform_info_primary.as_ref())
2186 .map(|(s, ti)| (s.clone(), ti.transform_length_0 as usize));
2187 let sec = sub
2188 .tools
2189 .scaled_spec_secondary
2190 .as_ref()
2191 .zip(sub.tools.transform_info_secondary.as_ref())
2192 .map(|(s, ti)| (s.clone(), ti.transform_length_0 as usize));
2193 let tables = sub.tools.aspx_frequency_tables.clone();
2194 let cfg = sub.tools.aspx_config;
2195 // add_harmonic flags per channel: prefer the 2-channel
2196 // hfgen payload when present, else fall back to the 1-ch
2197 // one for the primary channel (secondary inherits nothing
2198 // in that case — the 1-ch hfgen only covers one channel).
2199 let (ah_p, ah_s) = if let Some(h2) = sub.tools.aspx_hfgen_iwc_2ch.as_ref() {
2200 (
2201 Some(h2.add_harmonic[0].clone()),
2202 Some(h2.add_harmonic[1].clone()),
2203 )
2204 } else if let Some(h1) = sub.tools.aspx_hfgen_iwc_1ch.as_ref() {
2205 (Some(h1.add_harmonic.clone()), None)
2206 } else {
2207 (None, None)
2208 };
2209 // §5.7.6.4.1.3 Pseudocode 88 input — `aspx_tna_mode[ch][sbg]`.
2210 // 2-ch hfgen carries per-channel modes; 1-ch hfgen carries
2211 // a single channel's modes that we apply to the primary.
2212 let (tna_p, tna_s) = if let Some(h2) = sub.tools.aspx_hfgen_iwc_2ch.as_ref() {
2213 (Some(h2.tna_mode[0].clone()), Some(h2.tna_mode[1].clone()))
2214 } else if let Some(h1) = sub.tools.aspx_hfgen_iwc_1ch.as_ref() {
2215 (Some(h1.tna_mode.clone()), None)
2216 } else {
2217 (None, None)
2218 };
2219 (
2220 pri,
2221 sec,
2222 tables,
2223 cfg,
2224 sub.tools.aspx_framing_primary.clone(),
2225 sub.tools.aspx_framing_secondary.clone(),
2226 sub.tools.aspx_data_sig_primary.clone(),
2227 sub.tools.aspx_data_sig_secondary.clone(),
2228 sub.tools.aspx_data_noise_primary.clone(),
2229 sub.tools.aspx_data_noise_secondary.clone(),
2230 sub.tools.aspx_qmode_env_primary,
2231 sub.tools.aspx_qmode_env_secondary,
2232 sub.tools.aspx_delta_dir_primary.clone(),
2233 sub.tools.aspx_delta_dir_secondary.clone(),
2234 ah_p,
2235 ah_s,
2236 tna_p,
2237 tna_s,
2238 )
2239 } else {
2240 (
2241 None, None, None, None, None, None, None, None, None, None, None, None, None, None,
2242 None, None, None, None,
2243 )
2244 };
2245 // If the ASPX I-frame pipeline populated derived frequency
2246 // tables + config, run the A-SPX bandwidth-extension on top of
2247 // the IMDCT low-band PCM.
2248 let use_aspx_ext = aspx_tables.is_some() && aspx_cfg.is_some();
2249 let num_ts_in_ats = aspx::num_ts_in_ats(info.frame_length.max(1));
2250 // Round 43: per-channel companding mode from the parsed
2251 // `companding_control()`. For mono / stereo CPE paths the
2252 // grouping is `companding_control(1)` / `companding_control(2)`
2253 // — i.e. compand_on[0] is the primary channel, compand_on[1]
2254 // is the secondary (or the sole entry mirrors via sync_flag).
2255 let (compand_mode_pri, compand_mode_sec) = self
2256 .last_substream
2257 .as_ref()
2258 .map(|sub| {
2259 let cc = sub.tools.companding.as_ref();
2260 (
2261 Self::five_x_compand_mode_for_slot(cc, 0),
2262 Self::five_x_compand_mode_for_slot(cc, 1),
2263 )
2264 })
2265 .unwrap_or((aspx::CompandingMode::Off, aspx::CompandingMode::Off));
2266 // Round 43: §5.7.5.2 sb0 selection — for the ASPX_ACPL_1 codec
2267 // mode the companding tool starts at `acpl_qmf_band` instead of
2268 // `aspx_xover_band`. Both the stereo CPE ASPX_ACPL_1 path and
2269 // the 5_X ASPX_ACPL_1 path read this from
2270 // `acpl_config_1ch_partial.qmf_band`. `None` for any other
2271 // codec mode → falls back to `tables.sbx`.
2272 let compand_sb0_override: Option<u32> = self.last_substream.as_ref().and_then(|sub| {
2273 let stereo_acpl1 =
2274 matches!(sub.tools.stereo_mode, Some(asf::StereoCodecMode::AspxAcpl1));
2275 let five_x_acpl1 = matches!(
2276 sub.tools.five_x_mode,
2277 Some(crate::mch::FiveXCodecMode::AspxAcpl1)
2278 );
2279 if stereo_acpl1 || five_x_acpl1 {
2280 sub.tools
2281 .acpl_config_1ch_partial
2282 .as_ref()
2283 .map(|c| c.qmf_band as u32)
2284 } else {
2285 None
2286 }
2287 });
2288 // Make sure the per-channel A-SPX state vector is large enough.
2289 while self.aspx_ext_state.len() < channels as usize {
2290 self.aspx_ext_state.push(aspx::AspxChannelExtState::new());
2291 }
2292 // Same for the SSF synth state.
2293 while self.ssf_synth_state.len() < channels as usize {
2294 self.ssf_synth_state.push(ssf_synth::SsfSynthState::new());
2295 }
2296 // §5.7.7 A-CPL: when the substream parsed `acpl_config_1ch` +
2297 // `acpl_data_1ch` we run the channel-pair synthesis on the
2298 // ASPX-extended primary PCM and emit two channels. The path
2299 // owns the primary IMDCT + ASPX path so `pcm_per_channel[1]`
2300 // ends up populated by the synth's `z1` output instead of by a
2301 // duplicate-of-primary fallback.
2302 let use_acpl =
2303 channels as usize >= 2 && acpl_active_cfg.is_some() && acpl_active_data.is_some();
2304 // Round 45: stereo-CPE M=2 synced companding. When
2305 // `companding_control(2)` carried `sync_flag == 1` and the
2306 // primary / secondary cohort both feed the standalone ASPX
2307 // path (i.e. `!use_acpl` — ACPL_1 stereo only ASPX-extends
2308 // the M-channel via the `acpl1_active` branch and so falls
2309 // outside the synced cohort), the two channels share one
2310 // geometric-mean gain `g_synch(ts) = √(g_0 · g_1)` per
2311 // Pseudocode 121's `sync_flag == 1` branch instead of two
2312 // independent per-channel gains. For 5_X ASPX_ACPL_3 the
2313 // primary / secondary are the L / R carriers feeding
2314 // Pseudocode 118's `run_acpl_5x_mch_pcm`, so this puts the
2315 // ACPL_3 surround-pair driver on the same synced footing as
2316 // r44's 5_X SIMPLE/ASPX dispatch. Resolves to `None` for
2317 // `sync_flag == 0`, missing companding, or any non-sync
2318 // sub-branch — falling back to the per-channel
2319 // `aspx_extend_pcm` path below.
2320 let stereo_cpe_synced_mode: Option<aspx::CompandingMode> = self
2321 .last_substream
2322 .as_ref()
2323 .and_then(|sub| sub.tools.companding.as_ref())
2324 .and_then(|cc| Self::five_x_synced_mode(Some(cc)));
2325 let use_stereo_cpe_synced = use_aspx_ext
2326 && !use_acpl
2327 && channels as usize >= 2
2328 && stereo_cpe_synced_mode.is_some()
2329 && primary_in.is_some()
2330 && secondary_in.is_some()
2331 && primary_in.as_ref().map(|(_, n)| *n) == secondary_in.as_ref().map(|(_, n)| *n);
2332 if use_stereo_cpe_synced {
2333 // Synced stereo-CPE pipeline. IMDCT each channel, then
2334 // run the M=2 phase-1 / sync-apply / phase-2 helper.
2335 // SAFETY of the unwraps: guarded by `use_stereo_cpe_synced`
2336 // (use_aspx_ext, primary_in.is_some(), secondary_in.is_some(),
2337 // stereo_cpe_synced_mode.is_some()).
2338 let (p_scaled, p_n) = primary_in.as_ref().unwrap();
2339 let (s_scaled, s_n) = secondary_in.as_ref().unwrap();
2340 let n = *p_n;
2341 if n > 0 && n == samples as usize && *s_n == n && !pcm_per_channel.is_empty() {
2342 let pcm_pri_f = self.imdct_channel_f32(0, p_scaled, n);
2343 let pcm_sec_f = self.imdct_channel_f32(1, s_scaled, n);
2344 let pri_input = StereoCpeChannelInput {
2345 ch_index: 0,
2346 pcm_in: &pcm_pri_f,
2347 framing: framing_pri.as_ref(),
2348 sig: sig_pri.as_deref(),
2349 noise: noise_pri.as_deref(),
2350 qmode: qmode_pri,
2351 delta_dir: delta_dir_pri.as_ref(),
2352 add_harmonic: ah_pri.as_deref(),
2353 tna_mode: tna_pri.as_deref(),
2354 };
2355 let sec_input = StereoCpeChannelInput {
2356 ch_index: 1,
2357 pcm_in: &pcm_sec_f,
2358 framing: framing_sec.as_ref().or(framing_pri.as_ref()),
2359 sig: sig_sec.as_deref(),
2360 noise: noise_sec.as_deref(),
2361 qmode: qmode_sec.or(qmode_pri),
2362 delta_dir: delta_dir_sec.as_ref().or(delta_dir_pri.as_ref()),
2363 add_harmonic: ah_sec.as_deref().or(ah_pri.as_deref()),
2364 tna_mode: tna_sec.as_deref().or(tna_pri.as_deref()),
2365 };
2366 let (ext_pri, ext_sec) = self.extend_stereo_cpe_pair_with_sync_companding(
2367 &pri_input,
2368 &sec_input,
2369 aspx_tables.as_ref().unwrap(),
2370 aspx_cfg.as_ref().unwrap(),
2371 num_ts_in_ats,
2372 stereo_cpe_synced_mode.unwrap(),
2373 compand_sb0_override,
2374 );
2375 if pcm_per_channel.len() < 2 {
2376 while pcm_per_channel.len() < 2 {
2377 pcm_per_channel.push(None);
2378 }
2379 }
2380 pcm_per_channel[0] = Some(Self::pcm_f32_to_i16(&ext_pri));
2381 pcm_per_channel[1] = Some(Self::pcm_f32_to_i16(&ext_sec));
2382 }
2383 }
2384 if !use_stereo_cpe_synced {
2385 if let Some((scaled, n)) = primary_in {
2386 if n > 0 && n == samples as usize && !pcm_per_channel.is_empty() {
2387 if use_aspx_ext {
2388 let pcm_f = self.imdct_channel_f32(0, &scaled, n);
2389 let state = &mut self.aspx_ext_state[0];
2390 let extended = Self::aspx_extend_pcm(
2391 &pcm_f,
2392 aspx_tables.as_ref().unwrap(),
2393 aspx_cfg.as_ref().unwrap(),
2394 framing_pri.as_ref(),
2395 sig_pri.as_deref(),
2396 noise_pri.as_deref(),
2397 qmode_pri,
2398 delta_dir_pri.as_ref(),
2399 ah_pri.as_deref(),
2400 tna_pri.as_deref(),
2401 state,
2402 num_ts_in_ats,
2403 compand_mode_pri,
2404 compand_sb0_override,
2405 );
2406 if use_acpl {
2407 if let (Some(cfg), Some(data)) =
2408 (acpl_active_cfg.as_ref(), acpl_active_data.as_ref())
2409 {
2410 // ASPX_ACPL_1: feed both M (extended) and S
2411 // PCM into the stereo A-CPL. The S spectrum
2412 // is already in `secondary_in`; we IMDCT it
2413 // here without ASPX (the `aspx_data_1ch` in
2414 // ACPL_1 covers the M channel only).
2415 let acpl1_result = if acpl1_active {
2416 if let Some((s_scaled, s_n)) = secondary_in.as_ref() {
2417 if *s_n == n {
2418 let s_pcm = self.imdct_channel_f32(1, s_scaled, *s_n);
2419 acpl_synth::run_acpl_1ch_pcm_stereo(
2420 &extended,
2421 &s_pcm,
2422 cfg,
2423 data,
2424 &mut self.acpl_state,
2425 )
2426 } else {
2427 None
2428 }
2429 } else {
2430 None
2431 }
2432 } else {
2433 acpl_synth::run_acpl_1ch_pcm(
2434 &extended,
2435 cfg,
2436 data,
2437 &mut self.acpl_state,
2438 )
2439 };
2440 if let Some((left, right)) = acpl1_result {
2441 pcm_per_channel[0] = Some(Self::pcm_f32_to_i16(&left));
2442 pcm_per_channel[1] = Some(Self::pcm_f32_to_i16(&right));
2443 } else {
2444 pcm_per_channel[0] = Some(Self::pcm_f32_to_i16(&extended));
2445 }
2446 } else {
2447 pcm_per_channel[0] = Some(Self::pcm_f32_to_i16(&extended));
2448 }
2449 } else {
2450 pcm_per_channel[0] = Some(Self::pcm_f32_to_i16(&extended));
2451 }
2452 } else {
2453 pcm_per_channel[0] = Some(self.imdct_channel(0, &scaled, n));
2454 }
2455 }
2456 }
2457 if channels as usize >= 2 && !use_acpl {
2458 if let Some((scaled, n)) = secondary_in {
2459 if n > 0 && n == samples as usize {
2460 if use_aspx_ext {
2461 let pcm_f = self.imdct_channel_f32(1, &scaled, n);
2462 let state = &mut self.aspx_ext_state[1];
2463 let extended = Self::aspx_extend_pcm(
2464 &pcm_f,
2465 aspx_tables.as_ref().unwrap(),
2466 aspx_cfg.as_ref().unwrap(),
2467 framing_sec.as_ref().or(framing_pri.as_ref()),
2468 sig_sec.as_deref(),
2469 noise_sec.as_deref(),
2470 qmode_sec.or(qmode_pri),
2471 delta_dir_sec.as_ref().or(delta_dir_pri.as_ref()),
2472 ah_sec.as_deref().or(ah_pri.as_deref()),
2473 tna_sec.as_deref().or(tna_pri.as_deref()),
2474 state,
2475 num_ts_in_ats,
2476 compand_mode_sec,
2477 compand_sb0_override,
2478 );
2479 pcm_per_channel[1] = Some(Self::pcm_f32_to_i16(&extended));
2480 } else {
2481 pcm_per_channel[1] = Some(self.imdct_channel(1, &scaled, n));
2482 }
2483 }
2484 }
2485 }
2486 } // end `if !use_stereo_cpe_synced`
2487 // SSF synthesis path — if either ssf_data_* is populated and
2488 // the corresponding `pcm_per_channel[ch]` slot is still empty
2489 // (the ASF Huffman pipeline didn't fire because spec_frontend
2490 // was SSF), drive §5.2.3-5.2.7 → IMDCT to produce real PCM.
2491 // Synthesize each granule into a `num_blocks * n_mdct`-long
2492 // spectrum vector, then IMDCT each `n_mdct` block independently
2493 // and concat the resulting overlap-added PCM.
2494 if let Some(data) = ssf_primary.as_ref() {
2495 if !pcm_per_channel.is_empty() && pcm_per_channel[0].is_none() {
2496 let pcm = self.run_ssf_channel(0, data, samples as usize);
2497 if !pcm.is_empty() {
2498 pcm_per_channel[0] = Some(pcm);
2499 }
2500 }
2501 }
2502 if channels as usize >= 2 {
2503 if let Some(data) = ssf_secondary.as_ref() {
2504 if pcm_per_channel.len() >= 2 && pcm_per_channel[1].is_none() {
2505 let pcm = self.run_ssf_channel(1, data, samples as usize);
2506 if !pcm.is_empty() {
2507 pcm_per_channel[1] = Some(pcm);
2508 }
2509 }
2510 }
2511 }
2512 // §5.7.7.6.2 ASPX_ACPL_3 5_X synthesis (Pseudocode 118) —
2513 // When the substream parsed acpl_config_2ch + acpl_data_2ch and
2514 // the stereo-body path decoded the L/R carrier spectra, run the
2515 // full 5-channel A-CPL synthesis and populate channels 0..4.
2516 // Only fires when all five pcm_per_channel slots are still empty
2517 // (i.e. the standard stereo path didn't already claim them), or
2518 // when the frame is explicitly a 5_X ASPX_ACPL_3 substream.
2519 if five_x_acpl3_active {
2520 if let (Some(cfg), Some(data), Some(centre)) = (
2521 five_x_acpl3_cfg.as_ref(),
2522 five_x_acpl3_data.as_ref(),
2523 five_x_centre_spec.as_deref(),
2524 ) {
2525 // Carrier L and R come from pcm_per_channel[0] / [1] (already
2526 // filled by the stereo ASF / ASPX decode path above). If they
2527 // are present use them; otherwise zero-fill as placeholders so
2528 // the A-CPL synthesis still produces shaped Ls/Rs.
2529 let n = samples as usize;
2530 let pcm_l_f32: Vec<f32> = pcm_per_channel
2531 .first()
2532 .and_then(|p| p.as_ref())
2533 .map(|v| v.iter().map(|&s| s as f32 / 32767.0).collect())
2534 .unwrap_or_else(|| vec![0.0_f32; n]);
2535 let pcm_r_f32: Vec<f32> = pcm_per_channel
2536 .get(1)
2537 .and_then(|p| p.as_ref())
2538 .map(|v| v.iter().map(|&s| s as f32 / 32767.0).collect())
2539 .unwrap_or_else(|| vec![0.0_f32; n]);
2540 if let Some(out) = acpl_synth::run_acpl_5x_mch_pcm(
2541 &pcm_l_f32,
2542 &pcm_r_f32,
2543 centre,
2544 cfg,
2545 data,
2546 &mut self.acpl_5x_mch_state,
2547 ) {
2548 // Output channel mapping for 5.0/5.1:
2549 // ch0 = L, ch1 = R, ch2 = C, ch3 = Ls, ch4 = Rs.
2550 // Resize pcm_per_channel to 5 slots if needed.
2551 while pcm_per_channel.len() < 5 {
2552 pcm_per_channel.push(None);
2553 }
2554 pcm_per_channel[0] = Some(Self::pcm_f32_to_i16(&out.left));
2555 pcm_per_channel[1] = Some(Self::pcm_f32_to_i16(&out.right));
2556 pcm_per_channel[2] = Some(Self::pcm_f32_to_i16(&out.centre));
2557 pcm_per_channel[3] = Some(Self::pcm_f32_to_i16(&out.left_surround));
2558 pcm_per_channel[4] = Some(Self::pcm_f32_to_i16(&out.right_surround));
2559 }
2560 }
2561 }
2562 // §5.7.7.6.1 ASPX_ACPL_1 / ASPX_ACPL_2 5_X synthesis (Pseudocode 117) —
2563 // When the 5_X walker resolved `five_x_mode` to AspxAcpl1 / AspxAcpl2
2564 // and parsed the matching `acpl_config_1ch_*` + `acpl_data_1ch_pair`,
2565 // run the channel-pair synthesis on the L/R carrier PCM and emit
2566 // L / R / C / Ls / Rs.
2567 //
2568 // L/R carriers come from `pcm_per_channel[0]/[1]` (already filled
2569 // by the stereo ASF/ASPX decode path above when present, else
2570 // zero-filled placeholders). The centre carrier mirrors the
2571 // ACPL_3 path — `cfg0_centre_mono` exists in the tools struct
2572 // but lacks an end-to-end decode path; we use silence so the
2573 // QMF lengths line up. ACPL_1's Ls/Rs surround carriers are
2574 // similarly silence-placeholders for the same reason: A-CPL
2575 // synthesis still produces shaped Ls/Rs from the L/R carriers
2576 // and the pair parameters; the contribution from the surround
2577 // carriers (when those gain a real decode path) just adds in
2578 // on top.
2579 if five_x_pair_active && !five_x_acpl3_active {
2580 if let (Some(mode), Some(cfg), Some(data_1), Some(data_2)) = (
2581 five_x_pair_mode,
2582 five_x_pair_cfg.as_ref(),
2583 five_x_pair_data_1.as_ref(),
2584 five_x_pair_data_2.as_ref(),
2585 ) {
2586 // Round 37: IMDCT the parsed centre `mono_data(0)`
2587 // spectrum (Cfg0 trailing) into a real PCM carrier;
2588 // falls back to silence when `scaled_spec` is None
2589 // (LFE / SSF / Huffman miss) — see `imdct_mono_lfe_data_f32`.
2590 let centre_pcm = cfg0_centre_mono
2591 .as_ref()
2592 .and_then(|m| self.imdct_mono_lfe_data_f32(m, 2, samples as usize));
2593 // Round 40: standalone Ls/Rs surround mono walker for
2594 // ACPL_1's Mode 1 surround-driven path. The 5_X
2595 // ASPX_ACPL_1 inner walker now persists the joint-MDCT
2596 // residual pair (sSMP,3 / sSMP,4 per Table 181) on
2597 // `tools.acpl_1_residual_pair`; we IMDCT them here into
2598 // Ls/Rs PCM carriers and feed them as the `x3` / `x4`
2599 // inputs of Pseudocode 117. ACPL_2 mode never emits a
2600 // residual pair (no max_sfb_master in the walker), so
2601 // the detach is `None` for that path → silence — same
2602 // as the round-37 placeholder.
2603 //
2604 // Round 46 — ACPL_1 surround Ls/Rs ASPX extension:
2605 // SPEC-CONFIRMS-NOT-ASPX. Per ETSI TS 103 190-1 §4.2.6.6
2606 // Table 25 row `case ASPX_ACPL_1:` (the `5_X_codec_mode
2607 // == ASPX_ACPL_1` body parsed by
2608 // `parse_aspx_acpl_1_2_inner_body` in `mch.rs`) the
2609 // trailer order is `aspx_data_2ch()` (L/R primary
2610 // carriers) + `aspx_data_1ch()` (centre mono) + two
2611 // `acpl_data_1ch()` parameter sets — there is NO third
2612 // ASPX trailer for the surround Ls/Rs pair. The Ls/Rs
2613 // carriers are the joint-MDCT residual sSMP,3 / sSMP,4
2614 // straight out of the inner sf_data×2 walker; per
2615 // §5.7.5.2 / §5.7.6 ASPX BWE applies to the
2616 // M-channel-side carriers only (acpl_qmf_band-rooted
2617 // sb0 on the L/R primary pair + centre mono, never on
2618 // the residual surround pair). Feeding them raw into
2619 // Pseudocode 117 as `x3` / `x4` matches the spec — the
2620 // post-Pseudocode-117 surround output gets its
2621 // synthesis-bandwidth shape from the L/R carriers via
2622 // alpha/beta/decorrelator, not from independent
2623 // surround-pair extension. Same finding for the
2624 // matching M=2 surround-pair synced companding cohort:
2625 // no carriers means no companding to sync. Round-46
2626 // therefore wires no new surround-pair ASPX/companding
2627 // path here; the existing raw-PCM path is correct.
2628 let acpl_1_residual_pair = self
2629 .last_substream
2630 .as_ref()
2631 .map(|sub| sub.tools.acpl_1_residual_pair.clone())
2632 .unwrap_or([None, None]);
2633 // Round 41: §5.3.4.3.2 / Table 181 first-stage matrix —
2634 // when the 5_X ACPL_1 walker captured the two
2635 // `chparam_info()` payloads + the joint-MDCT residual
2636 // pair AND the inner `two_channel_data` carries
2637 // sSMP_A / sSMP_B spectra, mix per-sfb to produce
2638 // preliminary (L, R, Ls, Rs) spectra, IMDCT each, and
2639 // feed those PCMs into Pseudocode 117.
2640 //
2641 // When the SAP inputs aren't all available (ACPL_2 path,
2642 // or non-AspxAcpl1 mode, or any of the inputs missing)
2643 // fall through to the round-40 path: raw sSMP_3/sSMP_4
2644 // PCM as ls/rs, slots 0/1 untouched.
2645 let chparam_pair = self
2646 .last_substream
2647 .as_ref()
2648 .map(|sub| sub.tools.acpl_1_residual_chparam.clone())
2649 .unwrap_or([None, None]);
2650 let max_sfb_master_opt: Option<u32> = self
2651 .last_substream
2652 .as_ref()
2653 .and_then(|sub| sub.tools.acpl_1_residual_max_sfb_master);
2654 let inner_tcd_specs: Option<(Vec<f32>, Vec<f32>)> =
2655 self.last_substream.as_ref().and_then(|sub| {
2656 let tcd = sub.tools.two_channel_data.first()?;
2657 let a = tcd.scaled_spec_per_channel.first().cloned().flatten()?;
2658 let b = tcd.scaled_spec_per_channel.get(1).cloned().flatten()?;
2659 Some((a, b))
2660 });
2661 let sap_outputs: Option<asf::SapTable181Output> = match (
2662 mode,
2663 inner_tcd_specs.as_ref(),
2664 &chparam_pair,
2665 &acpl_1_residual_pair,
2666 max_sfb_master_opt,
2667 ) {
2668 (
2669 acpl_synth::Acpl5xPairMode::AspxAcpl1,
2670 Some((a_spec, b_spec)),
2671 [Some(cp0), Some(cp1)],
2672 [Some((tl3, s3)), Some((tl4, s4))],
2673 Some(max_sfb_master),
2674 ) if *tl3 == *tl4
2675 && *tl3 as usize == samples as usize
2676 && max_sfb_master > 0 =>
2677 {
2678 asf::apply_sap_table_181(
2679 a_spec,
2680 b_spec,
2681 s3,
2682 s4,
2683 &[cp0.clone(), cp1.clone()],
2684 max_sfb_master,
2685 *tl3,
2686 )
2687 }
2688 _ => None,
2689 };
2690 let (ls_pcm, rs_pcm) =
2691 if let Some((l_spec, r_spec, ls_spec, rs_spec)) = sap_outputs.as_ref() {
2692 // SAP path: replace pcm_per_channel[0]/[1] with the
2693 // mixed L/R PCM and pass mixed Ls/Rs PCM into the
2694 // pair dispatcher.
2695 let n = samples as usize;
2696 let l_pcm = self.imdct_channel_f32(0, l_spec, n);
2697 let r_pcm = self.imdct_channel_f32(1, r_spec, n);
2698 while pcm_per_channel.len() < 2 {
2699 pcm_per_channel.push(None);
2700 }
2701 pcm_per_channel[0] = Some(Self::pcm_f32_to_i16(&l_pcm));
2702 pcm_per_channel[1] = Some(Self::pcm_f32_to_i16(&r_pcm));
2703 let ls_pcm = self.imdct_channel_f32(3, ls_spec, n);
2704 let rs_pcm = self.imdct_channel_f32(4, rs_spec, n);
2705 (Some(ls_pcm), Some(rs_pcm))
2706 } else {
2707 let ls_pcm = acpl_1_residual_pair[0].as_ref().and_then(|(tl, scaled)| {
2708 if *tl as usize == samples as usize {
2709 Some(self.imdct_channel_f32(3, scaled, samples as usize))
2710 } else {
2711 None
2712 }
2713 });
2714 let rs_pcm = acpl_1_residual_pair[1].as_ref().and_then(|(tl, scaled)| {
2715 if *tl as usize == samples as usize {
2716 Some(self.imdct_channel_f32(4, scaled, samples as usize))
2717 } else {
2718 None
2719 }
2720 });
2721 (ls_pcm, rs_pcm)
2722 };
2723 self.dispatch_acpl_5x_pair(
2724 mode,
2725 cfg,
2726 data_1,
2727 data_2,
2728 samples as usize,
2729 centre_pcm.as_deref(),
2730 ls_pcm.as_deref(),
2731 rs_pcm.as_deref(),
2732 &mut pcm_per_channel,
2733 );
2734 }
2735 }
2736 // §5.7.7.6.3 Pseudocode 120 — 7_X ASPX_ACPL_1 / ASPX_ACPL_2
2737 // dispatch (mirrors the 5_X path above). Channel mapping is
2738 // Table 202 (channel_mode, add_ch_base) — for ACPL_1/_2 the
2739 // additional 2 channels (z6/z7 in Pseudocode 120) live outside
2740 // the A-CPL pair so they aren't generated here; we populate
2741 // slots 0..4 (L/R/C/Ls/Rs) and leave 5..7 for the per-channel
2742 // fallback path. The pair core itself is bit-equivalent to
2743 // Pseudocode 117 — same `(z0, z1) = ACplModule(...)` shape +
2744 // `z1 *= sqrt(2)` / `z3 *= sqrt(2)` scaling — modulo the extra
2745 // `add_ch_base == 0` z0/z2 sqrt(2) tweak which only fires when
2746 // the additional channels carry the L/R pair. Since we treat
2747 // the additional pair as silence here, that conditional scale
2748 // does not affect the produced 5-channel core.
2749 if seven_x_pair_active {
2750 if let (Some(mode), Some(cfg), Some(data_1), Some(data_2)) = (
2751 seven_x_pair_mode,
2752 seven_x_pair_cfg.as_ref(),
2753 seven_x_pair_data_1.as_ref(),
2754 seven_x_pair_data_2.as_ref(),
2755 ) {
2756 let centre_pcm = cfg0_centre_mono
2757 .as_ref()
2758 .and_then(|m| self.imdct_mono_lfe_data_f32(m, 2, samples as usize));
2759 // Round 40: same standalone Ls/Rs surround mono walker
2760 // as the 5_X path — the 7_X ASPX_ACPL_1 walker writes
2761 // to the same `acpl_1_residual_pair` slot. ACPL_2 path
2762 // detaches `None` (no residual pair).
2763 let acpl_1_residual_pair = self
2764 .last_substream
2765 .as_ref()
2766 .map(|sub| sub.tools.acpl_1_residual_pair.clone())
2767 .unwrap_or([None, None]);
2768 let ls_pcm = acpl_1_residual_pair[0].as_ref().and_then(|(tl, scaled)| {
2769 if *tl as usize == samples as usize {
2770 Some(self.imdct_channel_f32(3, scaled, samples as usize))
2771 } else {
2772 None
2773 }
2774 });
2775 let rs_pcm = acpl_1_residual_pair[1].as_ref().and_then(|(tl, scaled)| {
2776 if *tl as usize == samples as usize {
2777 Some(self.imdct_channel_f32(4, scaled, samples as usize))
2778 } else {
2779 None
2780 }
2781 });
2782 self.dispatch_acpl_5x_pair(
2783 mode,
2784 cfg,
2785 data_1,
2786 data_2,
2787 samples as usize,
2788 centre_pcm.as_deref(),
2789 ls_pcm.as_deref(),
2790 rs_pcm.as_deref(),
2791 &mut pcm_per_channel,
2792 );
2793 }
2794 }
2795 // Round 38 / 39: §5.3.4.3.1 / Table 180 — 5_X SIMPLE/ASPX
2796 // end-to-end decode. Round 38 wired Cfg2; round 39 wires Cfg0,
2797 // Cfg1, Cfg3. Mutually exclusive with the ACPL_3 / pair paths
2798 // above (they own different `five_x_mode` enums), so each cfg
2799 // fires only when the SIMPLE/ASPX pure-MDCT path is in scope.
2800 if five_x_simple_aspx_active && !five_x_acpl3_active && !five_x_pair_active {
2801 match five_x_coding_cfg {
2802 Some(crate::mch::FiveXCodingConfig::Cfg0Stereo2plusMono)
2803 if cfg_two_channel_data.len() >= 2 =>
2804 {
2805 let b_2ch = cfg_b_2ch_mode.unwrap_or(false);
2806 self.dispatch_5x_cfg0_simple_aspx(
2807 &cfg_two_channel_data[0],
2808 &cfg_two_channel_data[1],
2809 b_2ch,
2810 cfg0_centre_mono.as_ref(),
2811 cfg0_aspx_lr.as_ref(),
2812 cfg0_aspx_ls_rs.as_ref(),
2813 cfg0_aspx_centre.as_ref(),
2814 five_x_aspx_config,
2815 five_x_companding.as_ref(),
2816 num_ts_in_ats,
2817 samples as usize,
2818 &mut pcm_per_channel,
2819 );
2820 }
2821 Some(crate::mch::FiveXCodingConfig::Cfg1ThreeStereo) => {
2822 if let (Some(three), Some(tcd)) = (
2823 cfg_three_channel_data.as_ref(),
2824 cfg_two_channel_data.first(),
2825 ) {
2826 self.dispatch_5x_cfg1_simple_aspx(
2827 three,
2828 tcd,
2829 cfg1_aspx_lr.as_ref(),
2830 cfg1_aspx_ls_rs.as_ref(),
2831 cfg1_aspx_centre.as_ref(),
2832 five_x_aspx_config,
2833 five_x_companding.as_ref(),
2834 num_ts_in_ats,
2835 samples as usize,
2836 &mut pcm_per_channel,
2837 );
2838 }
2839 }
2840 Some(crate::mch::FiveXCodingConfig::Cfg2FourMono) => {
2841 if let Some(four) = cfg2_four_channel_data.as_ref() {
2842 self.dispatch_5x_cfg2_simple_aspx(
2843 four,
2844 cfg2_back_mono.as_ref(),
2845 cfg2_aspx_lr.as_ref(),
2846 cfg2_aspx_ls_rs.as_ref(),
2847 cfg2_aspx_centre.as_ref(),
2848 five_x_aspx_config,
2849 five_x_companding.as_ref(),
2850 num_ts_in_ats,
2851 samples as usize,
2852 &mut pcm_per_channel,
2853 );
2854 }
2855 }
2856 Some(crate::mch::FiveXCodingConfig::Cfg3Five) => {
2857 if let Some(five) = cfg_five_channel_data.as_ref() {
2858 self.dispatch_5x_cfg3_simple_aspx(
2859 five,
2860 cfg3_aspx_lr.as_ref(),
2861 cfg3_aspx_ls_rs.as_ref(),
2862 cfg3_aspx_centre.as_ref(),
2863 five_x_aspx_config,
2864 five_x_companding.as_ref(),
2865 num_ts_in_ats,
2866 samples as usize,
2867 &mut pcm_per_channel,
2868 );
2869 }
2870 }
2871 _ => {}
2872 }
2873 }
2874 // Round 91: 7_X SIMPLE/ASPX inner 5-channel core render (slots
2875 // 0..4). The 7_X SIMPLE/Cfg3Five path inherits the inner
2876 // `five_channel_data()` from the 5_X Table 29 layout (5 SCEs in
2877 // L/R/C/Ls/Rs order, identity SAP via 5x `chparam_info(sap_mode
2878 // = 0)`); the only difference from the 5_X dispatch is which
2879 // walker populated `tools.five_channel_data` (7_X here, vs 5_X
2880 // for the 5.0/5.1 paths). The 5_X dispatch fires the same
2881 // IMDCT/KBD/overlap-add chain regardless of which walker
2882 // populated the slot, so we route the 7_X-walker-produced
2883 // five_channel_data through it. With identity SAP no joint-MDCT
2884 // mixing happens at decode time so each output slot 0..4 reflects
2885 // only its own input SCE. ASPX trailers for the 7_X path land in
2886 // different `tools.*_aspx_*` slots (the 7_X walker has its own
2887 // ASPX trailer plumbing — out of scope here); pass `None` for
2888 // the trailer slots so the round-91 SIMPLE path reduces to
2889 // low-band only. Cfg0/Cfg1/Cfg2 7_X variants need their own
2890 // wiring (queued for follow-up rounds — they share the same
2891 // 5_X core dispatchers, just with the 7_X-specific trailing
2892 // `mono_data(0)` gate and ASPX trailer plumbing).
2893 if seven_x_simple_aspx_active
2894 && matches!(
2895 self.last_substream
2896 .as_ref()
2897 .and_then(|sub| sub.tools.seven_x_coding_config),
2898 Some(crate::mch::FiveXCodingConfig::Cfg3Five)
2899 )
2900 {
2901 if let Some(five) = self
2902 .last_substream
2903 .as_ref()
2904 .and_then(|sub| sub.tools.five_channel_data.clone())
2905 {
2906 self.dispatch_5x_cfg3_simple_aspx(
2907 &five,
2908 None,
2909 None,
2910 None,
2911 None,
2912 None,
2913 num_ts_in_ats,
2914 samples as usize,
2915 &mut pcm_per_channel,
2916 );
2917 }
2918 }
2919 // Round 39 / 40: §5.3.4.4.1 / Table 182 + Table 183 — 7_X
2920 // SIMPLE/ASPX additional-channel pair render. The walker populates
2921 // `seven_x_additional_channel_data` (two sf_data(ASF) bodies)
2922 // when `7_X_codec_mode in {SIMPLE, ASPX}`. Slots 5 / 6 (the F/G
2923 // preliminary outputs in Table 182) get the IMDCT'd low-band PCM.
2924 //
2925 // Round 40 wires the SAP a/b/c/d coefficient extraction
2926 // (`extract_sap_abcd` per Pseudocode 59) through Table 183's
2927 // 2-pair joint-stereo matrix when `b_use_sap_add_ch == true` AND
2928 // partner spectra (D, E for `coding_config in {0, 2, 3}` —
2929 // 3/4/0.x channel_mode) are present. The dispatch walks
2930 // (P, F) → (slot_high, slot_low) and (Q, G) → (slot_high+1,
2931 // slot_low+1) per-sfb in the spectral domain. With identity SAP
2932 // (`b_use_sap_add_ch == false`), the partner spectra are left
2933 // untouched at their 5.X-core slots and only F/G land at slots
2934 // 5/6 — matching the round-39 behaviour.
2935 //
2936 // The 7_X ACPL_1/_2 walker has its own additional-channel
2937 // handling per §5.3.4.4.2/.3 (z6/z7 in Pseudocode 120) — this
2938 // branch is gated on the SIMPLE/ASPX active-flag.
2939 if seven_x_simple_aspx_active {
2940 if let Some(add) = seven_x_additional_channel_data.as_ref() {
2941 // Resolve partner spectra + slots based on the active
2942 // 7_X coding_config. Per Table 183 row "3/4/0.x" (the
2943 // standard 7.0/7.1 layout that our 7_X walker handles)
2944 // the partner pair is (Ls, Rs) — slot 3 / slot 4 in our
2945 // 5.X-core dispatch; F/G lift to (Lb, Rb) on slot 5/6.
2946 let partner_slots: [usize; 2] = [3, 4];
2947 let (partner_d, partner_e): (Option<Vec<f32>>, Option<Vec<f32>>) =
2948 match five_x_coding_cfg {
2949 Some(crate::mch::FiveXCodingConfig::Cfg2FourMono) => {
2950 // 5_X cfg2 four_channel_data carries [L, R, Ls, Rs]
2951 // in indices [0, 1, 2, 3] per Table 180. The
2952 // surround pair lives at four[2]/four[3].
2953 let (d, e) = match cfg2_four_channel_data.as_ref() {
2954 Some(four) => (
2955 four.scaled_spec_per_channel.get(2).cloned().flatten(),
2956 four.scaled_spec_per_channel.get(3).cloned().flatten(),
2957 ),
2958 None => (None, None),
2959 };
2960 (d, e)
2961 }
2962 Some(crate::mch::FiveXCodingConfig::Cfg3Five) => {
2963 // 5_X cfg3 five_channel_data lays out [L, R, C,
2964 // Ls, Rs] per Table 180. Surround pair lives at
2965 // five[3]/five[4].
2966 let (d, e) = match cfg_five_channel_data.as_ref() {
2967 Some(five) => (
2968 five.scaled_spec_per_channel.get(3).cloned().flatten(),
2969 five.scaled_spec_per_channel.get(4).cloned().flatten(),
2970 ),
2971 None => (None, None),
2972 };
2973 (d, e)
2974 }
2975 Some(crate::mch::FiveXCodingConfig::Cfg1ThreeStereo) => {
2976 // 5_X cfg1 three_channel_data + two_channel_data:
2977 // surround pair lives at the trailing
2978 // two_channel_data[0]/[1] (slots 3/4 in our
2979 // dispatch). Use the parsed scaled_spec.
2980 let (d, e) = match cfg_two_channel_data.first() {
2981 Some(tcd) => (
2982 tcd.scaled_spec_per_channel.first().cloned().flatten(),
2983 tcd.scaled_spec_per_channel.get(1).cloned().flatten(),
2984 ),
2985 None => (None, None),
2986 };
2987 (d, e)
2988 }
2989 _ => (None, None),
2990 };
2991 let chparam_pair = self
2992 .last_substream
2993 .as_ref()
2994 .and_then(|sub| sub.tools.seven_x_add_chparam_info.as_ref().cloned());
2995 let partner_pair: Option<[&[f32]; 2]> =
2996 match (partner_d.as_ref(), partner_e.as_ref()) {
2997 (Some(d), Some(e)) => Some([d.as_slice(), e.as_slice()]),
2998 _ => None,
2999 };
3000 self.dispatch_7x_additional_channel_pair(
3001 add,
3002 partner_pair,
3003 partner_slots,
3004 chparam_pair.as_ref(),
3005 samples as usize,
3006 &mut pcm_per_channel,
3007 );
3008 }
3009 }
3010 // Round 80: 5.1 / 7.1 LFE channel render. When the 5_X / 7_X
3011 // walker parsed a `mono_data(b_lfe = 1)` payload (per §4.2.6.6
3012 // Table 25 `if (b_has_lfe) mono_data(1);` / §4.2.6.14 Table 33
3013 // equivalent) the LFE scaled spectrum lives on
3014 // `tools.lfe_mono_data.scaled_spec`. IMDCT it into the trailing
3015 // LFE PCM slot — slot 5 for 5.1 (after L/R/C/Ls/Rs) and slot 7
3016 // for 7.1 (after L/R/C/Ls/Rs/Lb/Rb).
3017 if channels == 6 || channels == 8 {
3018 let lfe_slot = (channels as usize) - 1;
3019 let lfe_mono = self
3020 .last_substream
3021 .as_ref()
3022 .and_then(|sub| sub.tools.lfe_mono_data.clone());
3023 if let Some(lfe) = lfe_mono.as_ref() {
3024 if let Some(pcm_f) = self.imdct_mono_lfe_data_f32(lfe, lfe_slot, samples as usize) {
3025 while pcm_per_channel.len() <= lfe_slot {
3026 pcm_per_channel.push(None);
3027 }
3028 pcm_per_channel[lfe_slot] = Some(Self::pcm_f32_to_i16(&pcm_f));
3029 }
3030 }
3031 }
3032 self.last_info = Some(info);
3033 let byte_count = (samples as usize) * (channels as usize) * 2; // S16 interleaved.
3034 let any_decoded = pcm_per_channel.iter().any(|p| p.is_some());
3035 let data = if any_decoded {
3036 let mut buf = vec![0u8; byte_count];
3037 // Channel fallback: if only channel 0 was decoded for a
3038 // multi-channel stream (e.g. a stereo frame whose CPE body
3039 // didn't parse), duplicate it across the remaining slots so
3040 // the output is audible rather than one-sided.
3041 let fallback = pcm_per_channel[0].clone();
3042 for i in 0..samples as usize {
3043 for c in 0..channels as usize {
3044 let sample = pcm_per_channel
3045 .get(c)
3046 .and_then(|p| p.as_ref())
3047 .or(fallback.as_ref())
3048 .and_then(|p| p.get(i).copied())
3049 .unwrap_or(0);
3050 let le = sample.to_le_bytes();
3051 let off = (i * channels as usize + c) * 2;
3052 if off + 1 < buf.len() {
3053 buf[off] = le[0];
3054 buf[off + 1] = le[1];
3055 }
3056 }
3057 }
3058 vec![buf]
3059 } else {
3060 vec![vec![0u8; byte_count]]
3061 };
3062 Ok(Frame::Audio(AudioFrame {
3063 samples,
3064 pts: pkt.pts,
3065 data,
3066 }))
3067 }
3068
3069 fn flush(&mut self) -> Result<()> {
3070 self.eof = true;
3071 Ok(())
3072 }
3073}
3074
3075#[cfg(test)]
3076mod tests {
3077 use super::*;
3078 use oxideav_core::bits::BitWriter;
3079
3080 fn build_minimal_toc() -> Vec<u8> {
3081 // Build a minimal single-presentation, single-substream AC-4 TOC
3082 // claiming 48 kHz, 24 fps, stereo (channel_mode prefix '10'),
3083 // b_iframe = 1.
3084 let mut bw = BitWriter::new();
3085 // bitstream_version = 0 (2 bits) — TS 103 190-1 v0 syntax body
3086 // follows. The parser dispatches `ac4_presentation_info()` only
3087 // when bitstream_version <= 1.
3088 bw.write_u32(0, 2);
3089 // sequence_counter = 7 (10 bits).
3090 bw.write_u32(7, 10);
3091 // b_wait_frames = 0.
3092 bw.write_u32(0, 1);
3093 // fs_index = 1 (48 kHz), frame_rate_index = 1 (24 fps).
3094 bw.write_u32(1, 1);
3095 bw.write_u32(1, 4);
3096 // b_iframe_global = 1, b_single_presentation = 1.
3097 bw.write_u32(1, 1);
3098 bw.write_u32(1, 1);
3099 // b_payload_base = 0.
3100 bw.write_u32(0, 1);
3101 // --- ac4_presentation_info() ---
3102 // b_single_substream = 1.
3103 bw.write_u32(1, 1);
3104 // presentation_version() = 0 (single '0').
3105 bw.write_u32(0, 1);
3106 // md_compat (3 bits), b_belongs_to_presentation_id = 0.
3107 bw.write_u32(0, 3);
3108 bw.write_u32(0, 1);
3109 // frame_rate_multiply_info: for fri=1 (index 1) it's a single
3110 // b_multiplier bit, 0.
3111 bw.write_u32(0, 1);
3112 // emdf_info(): emdf_version=0 (2b), key_id=0 (3b),
3113 // b_emdf_payloads_substream_info=0, emdf_reserved(): b_more=0.
3114 bw.write_u32(0, 2);
3115 bw.write_u32(0, 3);
3116 bw.write_u32(0, 1);
3117 bw.write_u32(0, 1);
3118 // ac4_substream_info():
3119 // channel_mode prefix '10' = stereo, fs_index==1 so
3120 // b_sf_multiplier=0, b_bitrate_info=0, b_content_type=0,
3121 // frame_rate_factor=1 -> 1 b_iframe bit (set),
3122 // substream_index = 0 (2 bits).
3123 bw.write_u32(0b10, 2); // channel_mode
3124 bw.write_u32(0, 1); // b_sf_multiplier
3125 bw.write_u32(0, 1); // b_bitrate_info
3126 bw.write_u32(0, 1); // b_content_type
3127 bw.write_u32(1, 1); // b_iframe
3128 bw.write_u32(0, 2); // substream_index
3129 // b_pre_virtualized = 0, b_add_emdf_substreams = 0.
3130 bw.write_u32(0, 1);
3131 bw.write_u32(0, 1);
3132 // substream_index_table(): n_substreams=1, b_size_present=0.
3133 bw.write_u32(1, 2);
3134 bw.write_u32(0, 1);
3135 // byte_align.
3136 bw.align_to_byte();
3137 bw.finish()
3138 }
3139
3140 #[test]
3141 fn decoder_emits_silence_with_correct_shape() {
3142 let mut bytes = build_minimal_toc();
3143 // Pad some substream body so the decoder has something to point
3144 // at (we don't touch it beyond the TOC).
3145 bytes.extend(vec![0u8; 64]);
3146 let params = CodecParameters::audio(CodecId::new("ac4"));
3147 let mut dec = Ac4Decoder::new(¶ms);
3148 let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
3149 dec.send_packet(&pkt).unwrap();
3150 let Frame::Audio(af) = dec.receive_frame().unwrap() else {
3151 panic!("expected audio");
3152 };
3153 // Per-frame channels / sample_rate / format are no longer carried
3154 // on AudioFrame — the byte count below implicitly checks stereo
3155 // S16 layout (1920 samples × 2 ch × 2 bytes).
3156 assert_eq!(af.samples, 1_920);
3157 assert_eq!(af.data.len(), 1);
3158 assert_eq!(af.data[0].len(), (1_920 * 2 * 2) as usize);
3159 // Samples are silent.
3160 assert!(af.data[0].iter().all(|&b| b == 0));
3161 let info = dec.last_info.as_ref().unwrap();
3162 assert_eq!(info.n_presentations, 1);
3163 assert_eq!(info.n_substreams, 1);
3164 assert_eq!(info.fs_index, 1);
3165 assert_eq!(info.frame_rate_index, 1);
3166 assert_eq!(info.frame_length, 1_920);
3167 assert!(info.b_iframe_global);
3168 }
3169
3170 fn build_mono_toc() -> Vec<u8> {
3171 // Single-presentation, single-substream AC-4 TOC claiming
3172 // 48 kHz, 24 fps, mono (channel_mode prefix '0'), b_iframe = 1.
3173 let mut bw = BitWriter::new();
3174 bw.write_u32(0, 2); // bitstream_version = 0 (TS 103 190-1 v0 syntax body follows)
3175 bw.write_u32(7, 10); // sequence_counter
3176 bw.write_u32(0, 1); // b_wait_frames
3177 bw.write_u32(1, 1); // fs_index = 1 (48 kHz)
3178 bw.write_u32(1, 4); // frame_rate_index = 1 (24 fps)
3179 bw.write_u32(1, 1); // b_iframe_global
3180 bw.write_u32(1, 1); // b_single_presentation
3181 bw.write_u32(0, 1); // b_payload_base
3182 // ac4_presentation_info:
3183 bw.write_u32(1, 1); // b_single_substream
3184 bw.write_u32(0, 1); // presentation_version = 0
3185 bw.write_u32(0, 3); // md_compat
3186 bw.write_u32(0, 1); // b_belongs_to_presentation_id
3187 bw.write_u32(0, 1); // frame_rate_multiply_info
3188 // emdf_info:
3189 bw.write_u32(0, 2);
3190 bw.write_u32(0, 3);
3191 bw.write_u32(0, 1);
3192 bw.write_u32(0, 1);
3193 // ac4_substream_info:
3194 bw.write_u32(0b0, 1); // channel_mode = 0 (mono) — prefix '0'
3195 bw.write_u32(0, 1); // b_sf_multiplier
3196 bw.write_u32(0, 1); // b_bitrate_info
3197 bw.write_u32(0, 1); // b_content_type
3198 bw.write_u32(1, 1); // b_iframe
3199 bw.write_u32(0, 2); // substream_index
3200 bw.write_u32(0, 1); // b_pre_virtualized
3201 bw.write_u32(0, 1); // b_add_emdf_substreams
3202 // substream_index_table:
3203 bw.write_u32(1, 2); // n_substreams - 1
3204 bw.write_u32(0, 1); // b_size_present
3205 bw.align_to_byte();
3206 bw.finish()
3207 }
3208
3209 /// Write a sect_len_incr sequence for a given section length.
3210 /// For n_sect_bits=3, esc=7: sect_len=1+7k+incr; emit k escapes
3211 /// followed by one non-escape.
3212 fn write_sect_len_incr(bw: &mut BitWriter, sect_len: u32, n_sect_bits: u32, esc: u32) {
3213 // sect_len = 1 + esc*k + incr where 0 <= incr < esc.
3214 let base = sect_len.saturating_sub(1);
3215 let k = base / esc;
3216 let incr = base % esc;
3217 for _ in 0..k {
3218 bw.write_u32(esc, n_sect_bits);
3219 }
3220 bw.write_u32(incr, n_sect_bits);
3221 }
3222
3223 /// Build an ac4_substream() body for mono, SIMPLE mode, ASF frontend,
3224 /// long frame, num_window_groups=1, with a single spectral band
3225 /// containing small quantised values so the decoder can produce
3226 /// non-silent audio.
3227 fn build_mono_asf_substream_body(tl: u32, max_sfb: u32) -> Vec<u8> {
3228 use crate::huffman;
3229 let mut bw = BitWriter::new();
3230 // audio_size_value (15 bits) — placeholder 200.
3231 bw.write_u32(200, 15);
3232 bw.write_bit(false); // b_more_bits = 0
3233 bw.align_to_byte();
3234 // audio_data() for channel_mode=0 (mono), b_iframe=1:
3235 // mono_codec_mode = 0 (SIMPLE)
3236 bw.write_u32(0, 1);
3237 // mono_data(0):
3238 // spec_frontend = 0 (ASF)
3239 bw.write_u32(0, 1);
3240 // asf_transform_info() — b_long_frame = 1.
3241 bw.write_bit(true);
3242 // asf_psy_info(0, 0): max_sfb[0] in n_msfb_bits = 6.
3243 bw.write_u32(max_sfb, 6);
3244 // No grouping bits for long frame.
3245 // asf_section_data: one section covering 0..max_sfb with cb=5
3246 // (dim=2, signed). n_sect_bits = 3 (transf_length_idx=0 for
3247 // long frame).
3248 bw.write_u32(5, 4); // sect_cb
3249 write_sect_len_incr(&mut bw, max_sfb, 3, 7);
3250 // asf_spectral_data.
3251 let sfbo = crate::sfb_offset::sfb_offset_48(tl).unwrap();
3252 let end_line = sfbo[max_sfb as usize] as u32;
3253 let hcb = huffman::asf_hcb(5).unwrap();
3254 let pairs = end_line / 2;
3255 for _ in 0..pairs {
3256 bw.write_u32(hcb.cw[40], hcb.len[40] as u32);
3257 }
3258 // asf_scalefac_data: reference_scale_factor = 120.
3259 bw.write_u32(120, 8);
3260 // No dpcm_sf codewords needed — all-zero spectra means
3261 // max_quant_idx == 0 for every band.
3262 // asf_snf_data: b_snf_data_exists = 0.
3263 bw.write_u32(0, 1);
3264 bw.align_to_byte();
3265 while bw.byte_len() < 220 {
3266 bw.write_u32(0, 8);
3267 }
3268 bw.finish()
3269 }
3270
3271 #[test]
3272 fn decoder_mono_asf_decode_path_runs() {
3273 // Build a mono AC-4 frame and push it through the decoder.
3274 // We're not asserting specific PCM values — we're asserting the
3275 // full pipeline (TOC -> substream -> ASF data -> IMDCT) runs
3276 // without error on a well-formed synthetic packet.
3277 let mut bytes = build_mono_toc();
3278 let body = build_mono_asf_substream_body(1920, 10);
3279 bytes.extend(body);
3280 let params = CodecParameters::audio(CodecId::new("ac4"));
3281 let mut dec = Ac4Decoder::new(¶ms);
3282 let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
3283 dec.send_packet(&pkt).unwrap();
3284 let Frame::Audio(af) = dec.receive_frame().unwrap() else {
3285 panic!("expected audio");
3286 };
3287 // Mono frame, 48 kHz, 1920 samples at 24 fps.
3288 // Per-frame channels / sample_rate / format dropped — the byte
3289 // count of the S16 data plane implicitly checks the layout
3290 // (1920 samples × 1 ch × 2 bytes = 3840 bytes).
3291 assert_eq!(af.samples, 1_920);
3292 assert_eq!(af.data[0].len(), 1_920 * 2);
3293 // substream parse must have succeeded.
3294 let sub = dec.last_substream.as_ref().unwrap();
3295 assert!(sub.tools.transform_info_primary.is_some());
3296 // We wrote a frame with all-zero spectra, so PCM output should
3297 // be silent (no MDCT energy injected).
3298 assert!(af.data[0].iter().all(|&b| b == 0));
3299 }
3300
3301 /// Build an ac4_substream() body carrying a single non-zero
3302 /// quantised spectral line so the IMDCT produces a real waveform.
3303 fn build_mono_asf_substream_body_with_tone(tl: u32, max_sfb: u32) -> Vec<u8> {
3304 use crate::huffman;
3305 let mut bw = BitWriter::new();
3306 bw.write_u32(400, 15);
3307 bw.write_bit(false);
3308 bw.align_to_byte();
3309 bw.write_u32(0, 1); // mono_codec_mode = SIMPLE
3310 bw.write_u32(0, 1); // spec_frontend = ASF
3311 bw.write_bit(true); // b_long_frame
3312 bw.write_u32(max_sfb, 6); // max_sfb[0]
3313 bw.write_u32(5, 4); // sect_cb
3314 write_sect_len_incr(&mut bw, max_sfb, 3, 7);
3315 let sfbo = crate::sfb_offset::sfb_offset_48(tl).unwrap();
3316 let end_line = sfbo[max_sfb as usize] as u32;
3317 // Emit one pair where the first line is +1 and rest zero.
3318 // HCB5 is signed. cb_mod=9, cb_off=4. For (1, 0): cb_idx = (1+4)*9 + (0+4) = 49.
3319 let hcb = huffman::asf_hcb(5).unwrap();
3320 bw.write_u32(hcb.cw[49], hcb.len[49] as u32);
3321 let pairs = end_line / 2;
3322 for _ in 1..pairs {
3323 bw.write_u32(hcb.cw[40], hcb.len[40] as u32);
3324 }
3325 // scalefac_data: reference_scale_factor = 120. sfb 0 has mqi=1
3326 // so first_scf_found triggers, sf_gain[0] = 2^((120-100)/4) = 32.
3327 bw.write_u32(120, 8);
3328 // snf: b_snf_data_exists = 0.
3329 bw.write_u32(0, 1);
3330 bw.align_to_byte();
3331 while bw.byte_len() < 420 {
3332 bw.write_u32(0, 8);
3333 }
3334 bw.finish()
3335 }
3336
3337 #[test]
3338 fn decoder_mono_asf_single_tone_produces_nonsilent_pcm() {
3339 // This exercises the full Huffman-driven ASF data path with a
3340 // synthetic frame that encodes a single +1 quantised spectral
3341 // line at bin 0 (sfb 0). Dequantisation gives a value of 1.0
3342 // * 2^((120-100)/4) = 32.0. After IMDCT + windowing the PCM
3343 // output should have nonzero energy (signal injected at the
3344 // DC bin produces a bias + ripple).
3345 let mut bytes = build_mono_toc();
3346 let body = build_mono_asf_substream_body_with_tone(1920, 10);
3347 bytes.extend(body);
3348 let params = CodecParameters::audio(CodecId::new("ac4"));
3349 let mut dec = Ac4Decoder::new(¶ms);
3350 let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
3351 dec.send_packet(&pkt).unwrap();
3352 let Frame::Audio(af) = dec.receive_frame().unwrap() else {
3353 panic!("expected audio");
3354 };
3355 assert_eq!(af.samples, 1_920);
3356 // Substream parse must have succeeded and scaled spectra is
3357 // populated.
3358 let sub = dec.last_substream.as_ref().unwrap();
3359 let scaled = sub.tools.scaled_spec_primary.as_ref().unwrap();
3360 // sfb 0 spans bins 0..4 (per SFB_OFFSET_1920[0..=1] = [0, 4]).
3361 // First non-zero value should be at bin 0.
3362 assert!(scaled[0].abs() > 0.0);
3363 // PCM should have non-trivial energy.
3364 let samples_i16: Vec<i16> = af.data[0]
3365 .chunks_exact(2)
3366 .map(|c| i16::from_le_bytes([c[0], c[1]]))
3367 .collect();
3368 let nonzero_count = samples_i16.iter().filter(|&&s| s != 0).count();
3369 assert!(
3370 nonzero_count > 100,
3371 "expected non-silent PCM, got {nonzero_count} non-zero samples",
3372 );
3373 let energy: i64 = samples_i16.iter().map(|&s| (s as i64) * (s as i64)).sum();
3374 assert!(energy > 0, "zero-energy output");
3375 }
3376
3377 /// Build a stereo SIMPLE ac4_substream() body with
3378 /// `b_enable_mdct_stereo_proc == 0` (split-MDCT path). `cb_idx_l`
3379 /// and `cb_idx_r` inject different HCB5 codewords at the first
3380 /// spectral pair of each channel so L and R carry different tones.
3381 fn build_stereo_asf_split_body_with_tones(
3382 tl: u32,
3383 max_sfb: u32,
3384 cb_idx_l: usize,
3385 cb_idx_r: usize,
3386 ) -> Vec<u8> {
3387 use crate::huffman;
3388 let mut bw = BitWriter::new();
3389 // audio_size_value = 800 (15 bits); b_more_bits = 0.
3390 bw.write_u32(800, 15);
3391 bw.write_bit(false);
3392 bw.align_to_byte();
3393 // stereo_codec_mode = SIMPLE (0b00, 2 bits).
3394 bw.write_u32(0, 2);
3395 // b_enable_mdct_stereo_proc = 0.
3396 bw.write_bit(false);
3397 // --- Left channel ---
3398 bw.write_u32(0, 1); // spec_frontend_l = ASF
3399 bw.write_bit(true); // b_long_frame
3400 bw.write_u32(max_sfb, 6); // max_sfb[0]
3401 // --- Right channel ---
3402 bw.write_u32(0, 1); // spec_frontend_r = ASF
3403 bw.write_bit(true); // b_long_frame
3404 bw.write_u32(max_sfb, 6); // max_sfb[0]
3405 // sf_data(spec_frontend_l): section_data + spectral + scalefac + snf.
3406 let sfbo = crate::sfb_offset::sfb_offset_48(tl).unwrap();
3407 let end_line = sfbo[max_sfb as usize] as u32;
3408 let hcb = huffman::asf_hcb(5).unwrap();
3409 // Section 0 covers [0..max_sfb) with sect_cb = 5.
3410 bw.write_u32(5, 4);
3411 write_sect_len_incr(&mut bw, max_sfb, 3, 7);
3412 // Spectral: emit cb_idx_l for pair 0, then cb_idx 40 for the rest.
3413 bw.write_u32(hcb.cw[cb_idx_l], hcb.len[cb_idx_l] as u32);
3414 let pairs = end_line / 2;
3415 for _ in 1..pairs {
3416 bw.write_u32(hcb.cw[40], hcb.len[40] as u32);
3417 }
3418 // scalefac: reference_scale_factor = 120.
3419 bw.write_u32(120, 8);
3420 // snf: b_snf_data_exists = 0.
3421 bw.write_u32(0, 1);
3422 // sf_data(spec_frontend_r): same pattern, different tone.
3423 bw.write_u32(5, 4);
3424 write_sect_len_incr(&mut bw, max_sfb, 3, 7);
3425 bw.write_u32(hcb.cw[cb_idx_r], hcb.len[cb_idx_r] as u32);
3426 for _ in 1..pairs {
3427 bw.write_u32(hcb.cw[40], hcb.len[40] as u32);
3428 }
3429 bw.write_u32(120, 8);
3430 bw.write_u32(0, 1);
3431 bw.align_to_byte();
3432 while bw.byte_len() < 820 {
3433 bw.write_u32(0, 8);
3434 }
3435 bw.finish()
3436 }
3437
3438 #[test]
3439 fn decoder_stereo_cpe_split_emits_two_channel_nonsilent_pcm() {
3440 // Stereo CPE, SIMPLE split-MDCT path: hand-craft a packet with
3441 // one HCB5 tone on L and a different HCB5 tone on R. Both
3442 // channels must carry real PCM (non-silent), and their sample
3443 // streams must differ.
3444 let mut bytes = build_minimal_toc(); // stereo TOC — channel_mode '10'
3445 // cb_idx=49 is (q0=1, q1=0); cb_idx=58 is (q0=2, q1=0).
3446 // Different tones -> different PCM per channel.
3447 let body = build_stereo_asf_split_body_with_tones(1920, 10, 49, 58);
3448 bytes.extend(body);
3449 let params = CodecParameters::audio(CodecId::new("ac4"));
3450 let mut dec = Ac4Decoder::new(¶ms);
3451 let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
3452 dec.send_packet(&pkt).unwrap();
3453 let Frame::Audio(af) = dec.receive_frame().unwrap() else {
3454 panic!("expected audio");
3455 };
3456 assert_eq!(af.samples, 1_920);
3457 // Both per-channel spectra should be populated.
3458 let sub = dec.last_substream.as_ref().unwrap();
3459 assert!(
3460 sub.tools.scaled_spec_primary.is_some(),
3461 "L spectrum missing"
3462 );
3463 assert!(
3464 sub.tools.scaled_spec_secondary.is_some(),
3465 "R spectrum missing"
3466 );
3467 // Decode PCM channel-wise from the interleaved S16 buffer.
3468 let buf = &af.data[0];
3469 assert_eq!(buf.len(), (1_920 * 2 * 2) as usize);
3470 let mut l: Vec<i16> = Vec::with_capacity(1_920);
3471 let mut r: Vec<i16> = Vec::with_capacity(1_920);
3472 for i in 0..1_920usize {
3473 let off_l = i * 4;
3474 let off_r = off_l + 2;
3475 l.push(i16::from_le_bytes([buf[off_l], buf[off_l + 1]]));
3476 r.push(i16::from_le_bytes([buf[off_r], buf[off_r + 1]]));
3477 }
3478 let e_l: i64 = l.iter().map(|&s| (s as i64) * (s as i64)).sum();
3479 let e_r: i64 = r.iter().map(|&s| (s as i64) * (s as i64)).sum();
3480 assert!(e_l > 0, "left channel silent");
3481 assert!(e_r > 0, "right channel silent");
3482 // Different tones -> different waveforms on L vs R.
3483 let nonzero_l = l.iter().filter(|&&s| s != 0).count();
3484 let nonzero_r = r.iter().filter(|&&s| s != 0).count();
3485 assert!(nonzero_l > 100, "L has too few samples: {nonzero_l}");
3486 assert!(nonzero_r > 100, "R has too few samples: {nonzero_r}");
3487 let differs = l.iter().zip(r.iter()).filter(|(a, b)| a != b).count();
3488 assert!(
3489 differs > 100,
3490 "L and R waveforms should differ (differing samples: {differs})"
3491 );
3492 }
3493
3494 /// Build a stereo SIMPLE ac4_substream() body with
3495 /// `b_enable_mdct_stereo_proc == 1` (joint M/S). Shared section
3496 /// data + scalefactors, two spectral residuals (M and S), a per
3497 /// active sfb `ms_used` flag, and an snf_data block.
3498 fn build_stereo_asf_joint_body(
3499 tl: u32,
3500 max_sfb: u32,
3501 cb_idx_m: usize,
3502 cb_idx_s: usize,
3503 ) -> Vec<u8> {
3504 use crate::huffman;
3505 let mut bw = BitWriter::new();
3506 bw.write_u32(800, 15);
3507 bw.write_bit(false);
3508 bw.align_to_byte();
3509 // stereo_codec_mode = SIMPLE.
3510 bw.write_u32(0, 2);
3511 // b_enable_mdct_stereo_proc = 1.
3512 bw.write_bit(true);
3513 // asf_transform_info() — b_long_frame = 1.
3514 bw.write_bit(true);
3515 // asf_psy_info(0, 0): max_sfb[0].
3516 bw.write_u32(max_sfb, 6);
3517 // Shared asf_section_data — one section cb=5 over [0..max_sfb).
3518 bw.write_u32(5, 4);
3519 write_sect_len_incr(&mut bw, max_sfb, 3, 7);
3520 let sfbo = crate::sfb_offset::sfb_offset_48(tl).unwrap();
3521 let end_line = sfbo[max_sfb as usize] as u32;
3522 let pairs = end_line / 2;
3523 let hcb = huffman::asf_hcb(5).unwrap();
3524 // Channel M spectrum.
3525 bw.write_u32(hcb.cw[cb_idx_m], hcb.len[cb_idx_m] as u32);
3526 for _ in 1..pairs {
3527 bw.write_u32(hcb.cw[40], hcb.len[40] as u32);
3528 }
3529 // Channel S spectrum.
3530 bw.write_u32(hcb.cw[cb_idx_s], hcb.len[cb_idx_s] as u32);
3531 for _ in 1..pairs {
3532 bw.write_u32(hcb.cw[40], hcb.len[40] as u32);
3533 }
3534 // Shared scalefac_data: reference_scale_factor = 120.
3535 bw.write_u32(120, 8);
3536 // ms_used[sfb] — one bit per active sfb. Only sfb 0 has energy
3537 // (cb != 0 and shared mqi > 0) so just one bit. Set to 1 so the
3538 // decoder runs the M/S -> L/R transform.
3539 bw.write_u32(1, 1);
3540 // snf_data: b_snf_data_exists = 0.
3541 bw.write_u32(0, 1);
3542 bw.align_to_byte();
3543 while bw.byte_len() < 820 {
3544 bw.write_u32(0, 8);
3545 }
3546 bw.finish()
3547 }
3548
3549 #[test]
3550 fn decoder_stereo_cpe_joint_ms_emits_two_channels() {
3551 // Joint-stereo M/S CPE with shared scalefactors. M has cb_idx=49
3552 // (q0=1,q1=0), S has cb_idx=40 (q0=0,q1=0 -> all zero). With
3553 // ms_used[0]=1, the inverse is L = M + S = M, R = M - S = M,
3554 // so both channels should be equal and non-silent.
3555 let mut bytes = build_minimal_toc(); // stereo TOC (channel_mode '10')
3556 let body = build_stereo_asf_joint_body(1920, 10, 49, 40);
3557 bytes.extend(body);
3558 let params = CodecParameters::audio(CodecId::new("ac4"));
3559 let mut dec = Ac4Decoder::new(¶ms);
3560 let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
3561 dec.send_packet(&pkt).unwrap();
3562 let Frame::Audio(af) = dec.receive_frame().unwrap() else {
3563 panic!("expected audio");
3564 };
3565 assert_eq!(af.samples, 1_920);
3566 let sub = dec.last_substream.as_ref().unwrap();
3567 assert!(sub.tools.mdct_stereo_proc, "joint-stereo flag missing");
3568 assert!(sub.tools.scaled_spec_primary.is_some());
3569 assert!(sub.tools.scaled_spec_secondary.is_some());
3570 // ms_used must have been read and the DC band flagged.
3571 let ms_used = sub.tools.ms_used.as_ref().unwrap();
3572 assert!(ms_used[0], "ms_used[0] should be true");
3573 // Both channels non-silent.
3574 let buf = &af.data[0];
3575 let mut l: Vec<i16> = Vec::with_capacity(1_920);
3576 let mut r: Vec<i16> = Vec::with_capacity(1_920);
3577 for i in 0..1_920usize {
3578 let off_l = i * 4;
3579 let off_r = off_l + 2;
3580 l.push(i16::from_le_bytes([buf[off_l], buf[off_l + 1]]));
3581 r.push(i16::from_le_bytes([buf[off_r], buf[off_r + 1]]));
3582 }
3583 let e_l: i64 = l.iter().map(|&s| (s as i64) * (s as i64)).sum();
3584 let e_r: i64 = r.iter().map(|&s| (s as i64) * (s as i64)).sum();
3585 assert!(e_l > 0 && e_r > 0, "expected non-silent L and R");
3586 // With S=0 and ms_used=1: L = M, R = M -> waveforms identical.
3587 let differing = l.iter().zip(r.iter()).filter(|(a, b)| a != b).count();
3588 assert!(
3589 differing < 4,
3590 "M/S inverse with S=0 should give L==R, got {differing} diffs"
3591 );
3592 }
3593
3594 #[test]
3595 fn aspx_extend_pcm_produces_non_silent_output() {
3596 // Smoke-test the wiring glue: hand a synthetic low-band PCM +
3597 // plausible frequency tables + config to the ASPX extension
3598 // helper and assert the output carries energy.
3599 let n_slots = 60usize;
3600 let n = n_slots * 64;
3601 let mut pcm = vec![0.0f32; n];
3602 let f = 500.0_f32 / 48_000.0_f32;
3603 for (i, s) in pcm.iter_mut().enumerate() {
3604 *s = (2.0 * std::f32::consts::PI * f * i as f32).sin();
3605 }
3606 let cfg = aspx::AspxConfig {
3607 quant_mode_env: aspx::AspxQuantStep::Fine,
3608 start_freq: 0,
3609 stop_freq: 0,
3610 master_freq_scale: aspx::AspxMasterFreqScale::HighRes,
3611 interpolation: false,
3612 preflat: false,
3613 limiter: false,
3614 noise_sbg: 0,
3615 num_env_bits_fixfix: 0,
3616 freq_res_mode: aspx::AspxFreqResMode::Signalled,
3617 };
3618 let tables = aspx::derive_aspx_frequency_tables(&cfg, 0).unwrap();
3619 let mut state = aspx::AspxChannelExtState::new();
3620 let out = Ac4Decoder::aspx_extend_pcm(
3621 &pcm,
3622 &tables,
3623 &cfg,
3624 None,
3625 None,
3626 None,
3627 None,
3628 None,
3629 None,
3630 None,
3631 &mut state,
3632 1,
3633 aspx::CompandingMode::Off,
3634 None,
3635 );
3636 assert_eq!(out.len(), pcm.len());
3637 // Steady-state energy must be non-zero in the far tail (post
3638 // QMF settling).
3639 let start = 1200usize;
3640 let mut energy = 0.0f64;
3641 let mut nonzero = 0usize;
3642 for &s in &out[start..] {
3643 let v = s as f64;
3644 energy += v * v;
3645 if s != 0.0 {
3646 nonzero += 1;
3647 }
3648 }
3649 assert!(
3650 energy > 1e-4,
3651 "aspx_extend_pcm output has no energy ({energy})"
3652 );
3653 assert!(
3654 nonzero > (out.len() - start) / 2,
3655 "too few non-zero samples: {nonzero}"
3656 );
3657 }
3658
3659 #[test]
3660 fn aspx_extend_pcm_with_tna_mode_diverges_from_bare_tile_copy() {
3661 // Same synthetic input as `aspx_extend_pcm_produces_non_silent_output`
3662 // but supply `tna_mode = [Heavy]` and a FIXFIX framing so the
3663 // §5.7.6.4.1.3 chirp + α0 + α1 TNS body activates. The output
3664 // must differ from the bare tile-copy result (Pseudocode 89
3665 // adds two correction terms that are zero only when chirp == 0
3666 // or α == 0, and we'd hit neither here).
3667 //
3668 // Use n_slots = 32 with num_ts_in_ats = 2 → num_aspx_ts = 16,
3669 // which is one of the eight values Table 194 / 192 supports.
3670 let n_slots = 32usize;
3671 let n = n_slots * 64;
3672 let mut pcm = vec![0.0f32; n];
3673 let f = 1500.0_f32 / 48_000.0_f32; // a tone in the low band
3674 for (i, s) in pcm.iter_mut().enumerate() {
3675 *s = (2.0 * std::f32::consts::PI * f * i as f32).sin();
3676 }
3677 let cfg = aspx::AspxConfig {
3678 quant_mode_env: aspx::AspxQuantStep::Fine,
3679 start_freq: 0,
3680 stop_freq: 0,
3681 master_freq_scale: aspx::AspxMasterFreqScale::HighRes,
3682 interpolation: false,
3683 preflat: false,
3684 limiter: false,
3685 noise_sbg: 0,
3686 num_env_bits_fixfix: 0,
3687 freq_res_mode: aspx::AspxFreqResMode::Signalled,
3688 };
3689 let tables = aspx::derive_aspx_frequency_tables(&cfg, 0).unwrap();
3690 // Build a FIXFIX framing with num_env=1, num_noise=1 so that
3691 // derive_fixfix_atsg(num_aspx_ts, 1, 1) returns Some(...).
3692 let framing = aspx::AspxFraming {
3693 int_class: aspx::AspxIntClass::FixFix,
3694 num_env: 1,
3695 num_noise: 1,
3696 freq_res: vec![false],
3697 var_bord_left: None,
3698 var_bord_right: None,
3699 num_rel_left: 0,
3700 num_rel_right: 0,
3701 rel_bord_left: vec![],
3702 rel_bord_right: vec![],
3703 tsg_ptr: None,
3704 };
3705 let num_sbg_noise = tables.sbg_noise.len().saturating_sub(1).max(1);
3706 let tna_mode_heavy = vec![3_u8; num_sbg_noise]; // all "Heavy"
3707 let tna_mode_zero = vec![0_u8; num_sbg_noise]; // all "None"
3708
3709 // Run twice: once with Heavy TNS, once with bare tile copy.
3710 let mut state_a = aspx::AspxChannelExtState::new();
3711 let out_tns = Ac4Decoder::aspx_extend_pcm(
3712 &pcm,
3713 &tables,
3714 &cfg,
3715 Some(&framing),
3716 None,
3717 None,
3718 None,
3719 None,
3720 None,
3721 Some(&tna_mode_heavy),
3722 &mut state_a,
3723 2,
3724 aspx::CompandingMode::Off,
3725 None,
3726 );
3727 let mut state_b = aspx::AspxChannelExtState::new();
3728 let out_bare = Ac4Decoder::aspx_extend_pcm(
3729 &pcm,
3730 &tables,
3731 &cfg,
3732 Some(&framing),
3733 None,
3734 None,
3735 None,
3736 None,
3737 None,
3738 Some(&tna_mode_zero),
3739 &mut state_b,
3740 2,
3741 aspx::CompandingMode::Off,
3742 None,
3743 );
3744 assert_eq!(out_tns.len(), pcm.len());
3745 assert_eq!(out_bare.len(), pcm.len());
3746 // Outputs must differ in the post-settling region.
3747 let start = 640usize;
3748 let mut diffs = 0usize;
3749 for (a, b) in out_tns[start..].iter().zip(out_bare[start..].iter()) {
3750 if (a - b).abs() > 1e-6 {
3751 diffs += 1;
3752 }
3753 }
3754 assert!(
3755 diffs > (out_tns.len() - start) / 100,
3756 "TNS path didn't diverge from bare tile copy: {diffs} diffs"
3757 );
3758 // TNS path must also have advanced state: tns.tna_mode_prev /
3759 // chirp_prev / q_low_prev should now be populated.
3760 assert_eq!(state_a.tns.tna_mode_prev.len(), num_sbg_noise);
3761 assert_eq!(state_a.tns.chirp_prev.len(), num_sbg_noise);
3762 assert!(!state_a.q_low_prev.is_empty());
3763 }
3764
3765 #[test]
3766 fn decoder_handles_sync_wrapped_packet() {
3767 let raw = build_minimal_toc();
3768 let mut wrapped = vec![0xAC, 0x40];
3769 let fs = raw.len() as u16;
3770 wrapped.extend_from_slice(&fs.to_be_bytes());
3771 wrapped.extend_from_slice(&raw);
3772 let params = CodecParameters::audio(CodecId::new("ac4"));
3773 let mut dec = Ac4Decoder::new(¶ms);
3774 let pkt = Packet::new(0, TimeBase::new(1, 48_000), wrapped);
3775 dec.send_packet(&pkt).unwrap();
3776 let Frame::Audio(af) = dec.receive_frame().unwrap() else {
3777 panic!("expected audio");
3778 };
3779 assert_eq!(af.samples, 1_920);
3780 }
3781
3782 /// Round-31: end-to-end SSF synthesis test. Builds a synthetic
3783 /// SsfData via the public API (LONG_STRIDE I-frame, num_bands=12,
3784 /// predictor disabled), runs the §5.2.3-5.2.7 synth, and verifies
3785 /// the output is finite + bin layout matches the spec
3786 /// (num_bins == 140 for n_mdct=960 / num_bands=12 from
3787 /// SsfBinLayout). All-zero AC payload + all-zero envelope indices
3788 /// yields i_alloc=0 across all bands → noise-RNG-driven f_spec_invq.
3789 #[test]
3790 fn ssf_synth_long_stride_iframe_end_to_end() {
3791 use crate::ssf;
3792 use crate::ssf_synth;
3793 use oxideav_core::bits::{BitReader, BitWriter};
3794 // Build the same shape the asf walker will hand us: one
3795 // LONG_STRIDE I-granule with num_bands=12, n_mdct=960.
3796 let mut bw = BitWriter::new();
3797 bw.write_u32(0, 1); // stride_flag = LONG_STRIDE
3798 bw.write_u32(0, 3); // num_bands_minus12 = 0 → num_bands = 12
3799 // No per-block predictor loop iterations in this layout.
3800 // ssf_st_data():
3801 bw.write_u32(0, 5); // env_curr_band0_bits
3802 bw.write_u32(0, 1); // variance_preserving_flag
3803 bw.write_u32(0, 5); // alloc_offset_bits
3804 // ssf_ac_data() init + payload — pad ample zeros.
3805 for _ in 0..(30 + 256) {
3806 bw.write_bit(false);
3807 }
3808 bw.align_to_byte();
3809 let bytes = bw.finish();
3810 let mut br = BitReader::new(&bytes);
3811 let cfg = ssf::SsfFrameConfig::from_toc(1, 5, 960).unwrap();
3812 let mut walk_state = ssf::SsfChannelState::new();
3813 let data = ssf::parse_ssf_data(&mut br, true, &cfg, &mut walk_state).expect("ssf walker");
3814 // Now drive the synth.
3815 let mut synth_state = ssf_synth::SsfSynthState::new();
3816 let spec = ssf_synth::synthesize_ssf_data(&data, &mut synth_state);
3817 // One block of n_mdct=960 spectral lines.
3818 assert_eq!(spec.len(), 960);
3819 // All entries must be finite (RNG-driven noise on zero alloc).
3820 for (i, &v) in spec.iter().enumerate() {
3821 assert!(v.is_finite(), "bin {i} not finite: {v}");
3822 }
3823 // The first num_bins (140) coded lines are the synth output;
3824 // the tail is zero-padded.
3825 for &v in spec[140..].iter() {
3826 assert_eq!(v, 0.0);
3827 }
3828 }
3829
3830 // =====================================================================
3831 // §5.7.7.6.1 ASPX_ACPL_1 / ASPX_ACPL_2 5_X dispatch tests
3832 // (round 36 — wire Pseudocode 117 into Ac4Decoder::receive_frame)
3833 // =====================================================================
3834
3835 use crate::acpl::{
3836 AcplConfig1ch, AcplData1ch, AcplFramingData, AcplHuffParam, AcplInterpolationType,
3837 AcplQuantMode,
3838 };
3839 use crate::acpl_synth::Acpl5xPairMode;
3840
3841 /// Build a single Huffman parameter set with constant value across
3842 /// all bands (mirrors the helper in tests/acpl_5x_pipeline.rs).
3843 fn dispatch_huff_const(value: i32, num_bands: u32) -> AcplHuffParam {
3844 AcplHuffParam {
3845 values: vec![value; num_bands as usize],
3846 direction_time: false,
3847 }
3848 }
3849
3850 /// Build a stub `acpl_data_1ch()` carrying constant alpha/beta
3851 /// across one parameter set with smooth interpolation.
3852 fn dispatch_stub_data_1ch(alpha: i32, beta: i32, num_bands: u32) -> AcplData1ch {
3853 AcplData1ch {
3854 framing: AcplFramingData {
3855 interpolation_type: AcplInterpolationType::Smooth,
3856 num_param_sets_cod: 0,
3857 num_param_sets: 1,
3858 param_timeslots: Vec::new(),
3859 },
3860 alpha1: vec![dispatch_huff_const(alpha, num_bands)],
3861 beta1: vec![dispatch_huff_const(beta, num_bands)],
3862 }
3863 }
3864
3865 fn dispatch_stub_cfg(num_param_bands: u32) -> AcplConfig1ch {
3866 AcplConfig1ch {
3867 num_param_bands_id: 0,
3868 num_param_bands,
3869 quant_mode: AcplQuantMode::Coarse,
3870 qmf_band: 0,
3871 }
3872 }
3873
3874 /// Build an Ac4Decoder with a populated `pcm_per_channel` carrier
3875 /// pair (L/R) and run `dispatch_acpl_5x_pair` for ASPX_ACPL_2.
3876 /// Verify five channels land and centre/Ls/Rs are non-empty buffers.
3877 #[test]
3878 fn dispatch_acpl_5x_pair_aspx_acpl_2_emits_five_channels() {
3879 let params = CodecParameters::audio(CodecId::new("ac4"));
3880 let mut dec = Ac4Decoder::new(¶ms);
3881 // 1920 samples = 30 QMF slots — matches a 48 kHz / 24 fps frame.
3882 let n = 1_920usize;
3883 // Carrier PCM: low-amp alternating ±2000 to drive the QMF
3884 // analysis bank with finite energy.
3885 let carrier_l: Vec<i16> = (0..n)
3886 .map(|i| if i & 1 == 0 { 2_000_i16 } else { -2_000_i16 })
3887 .collect();
3888 let carrier_r: Vec<i16> = (0..n)
3889 .map(|i| if i & 1 == 0 { -1_500_i16 } else { 1_500_i16 })
3890 .collect();
3891 let mut pcm_per_channel: Vec<Option<Vec<i16>>> = vec![Some(carrier_l), Some(carrier_r)];
3892 let cfg = dispatch_stub_cfg(12);
3893 let data_1 = dispatch_stub_data_1ch(3, 1, cfg.num_param_bands);
3894 let data_2 = dispatch_stub_data_1ch(-2, 2, cfg.num_param_bands);
3895
3896 dec.dispatch_acpl_5x_pair(
3897 Acpl5xPairMode::AspxAcpl2,
3898 &cfg,
3899 &data_1,
3900 &data_2,
3901 n,
3902 None,
3903 None,
3904 None,
3905 &mut pcm_per_channel,
3906 );
3907
3908 assert!(
3909 pcm_per_channel.len() >= 5,
3910 "dispatch must grow pcm_per_channel to 5 slots, got {}",
3911 pcm_per_channel.len()
3912 );
3913 for (ch, slot) in pcm_per_channel.iter().enumerate().take(5) {
3914 let pcm = slot
3915 .as_ref()
3916 .unwrap_or_else(|| panic!("channel {ch} should be populated by dispatch"));
3917 assert_eq!(pcm.len(), n, "channel {ch} length");
3918 }
3919 // L and R must contain non-zero samples (carriers passed
3920 // through QMF analysis + synthesis with energy > 0).
3921 let l_energy: u64 = pcm_per_channel[0]
3922 .as_ref()
3923 .unwrap()
3924 .iter()
3925 .map(|&s| s.unsigned_abs() as u64)
3926 .sum();
3927 let r_energy: u64 = pcm_per_channel[1]
3928 .as_ref()
3929 .unwrap()
3930 .iter()
3931 .map(|&s| s.unsigned_abs() as u64)
3932 .sum();
3933 assert!(l_energy > 0, "left channel must carry energy");
3934 assert!(r_energy > 0, "right channel must carry energy");
3935 }
3936
3937 /// ASPX_ACPL_1 should run with the same shape but additionally
3938 /// allocate Ls/Rs surround carrier placeholders. With zero-filled
3939 /// surround placeholders, the output should still be five channels.
3940 #[test]
3941 fn dispatch_acpl_5x_pair_aspx_acpl_1_emits_five_channels() {
3942 let params = CodecParameters::audio(CodecId::new("ac4"));
3943 let mut dec = Ac4Decoder::new(¶ms);
3944 let n = 1_920usize;
3945 let carrier_l: Vec<i16> = (0..n)
3946 .map(|i| if i % 4 < 2 { 1_500_i16 } else { -1_500_i16 })
3947 .collect();
3948 let carrier_r: Vec<i16> = (0..n)
3949 .map(|i| if i % 4 < 2 { -1_200_i16 } else { 1_200_i16 })
3950 .collect();
3951 let mut pcm_per_channel: Vec<Option<Vec<i16>>> = vec![Some(carrier_l), Some(carrier_r)];
3952 let cfg = dispatch_stub_cfg(12);
3953 let data_1 = dispatch_stub_data_1ch(2, 1, cfg.num_param_bands);
3954 let data_2 = dispatch_stub_data_1ch(-3, 2, cfg.num_param_bands);
3955
3956 dec.dispatch_acpl_5x_pair(
3957 Acpl5xPairMode::AspxAcpl1,
3958 &cfg,
3959 &data_1,
3960 &data_2,
3961 n,
3962 None,
3963 None,
3964 None,
3965 &mut pcm_per_channel,
3966 );
3967
3968 assert!(pcm_per_channel.len() >= 5);
3969 for (ch, slot) in pcm_per_channel.iter().enumerate().take(5) {
3970 assert!(slot.is_some(), "channel {ch} should be populated");
3971 assert_eq!(slot.as_ref().unwrap().len(), n);
3972 }
3973 }
3974
3975 /// Round 40: standalone Ls/Rs surround mono walker — when the
3976 /// `acpl_1_residual_pair` is populated and we feed the IMDCT'd PCM
3977 /// as `ls_pcm` / `rs_pcm` to `dispatch_acpl_5x_pair`, the output
3978 /// surround channels (slots 3 / 4) must reflect non-zero energy
3979 /// from the residual carriers (replacing the round-37 silence
3980 /// placeholder).
3981 #[test]
3982 fn dispatch_acpl_5x_pair_with_real_ls_rs_carriers_emits_surround_energy() {
3983 let params = CodecParameters::audio(CodecId::new("ac4"));
3984 let mut dec = Ac4Decoder::new(¶ms);
3985 let n = 1_920usize;
3986 let carrier_l: Vec<i16> = (0..n).map(|i| (i % 200) as i16 * 30).collect();
3987 let carrier_r: Vec<i16> = (0..n).map(|i| ((i + 50) % 200) as i16 * 30).collect();
3988 let mut pcm_per_channel: Vec<Option<Vec<i16>>> = vec![Some(carrier_l), Some(carrier_r)];
3989 let cfg = dispatch_stub_cfg(12);
3990 let data_1 = dispatch_stub_data_1ch(2, 1, cfg.num_param_bands);
3991 let data_2 = dispatch_stub_data_1ch(-3, 2, cfg.num_param_bands);
3992 // Feed real Ls/Rs PCM (mimicking what the round-40 walker does:
3993 // IMDCT the parsed `acpl_1_residual_pair` spectra and pass the
3994 // PCM as the `x3` / `x4` inputs to Pseudocode 117).
3995 let ls_pcm: Vec<f32> = (0..n).map(|i| 0.05 * (i as f32 / n as f32)).collect();
3996 let rs_pcm: Vec<f32> = (0..n).map(|i| -0.05 * (i as f32 / n as f32)).collect();
3997
3998 dec.dispatch_acpl_5x_pair(
3999 Acpl5xPairMode::AspxAcpl1,
4000 &cfg,
4001 &data_1,
4002 &data_2,
4003 n,
4004 None,
4005 Some(&ls_pcm),
4006 Some(&rs_pcm),
4007 &mut pcm_per_channel,
4008 );
4009
4010 assert!(pcm_per_channel.len() >= 5);
4011 for (slot, entry) in pcm_per_channel.iter().enumerate().take(5) {
4012 assert!(entry.is_some(), "slot {slot} populated by dispatch");
4013 assert_eq!(entry.as_ref().unwrap().len(), n);
4014 }
4015 }
4016
4017 /// `dispatch_acpl_5x_pair` must early-return when the sample count
4018 /// isn't a multiple of NUM_QMF_SUBBANDS (64), leaving
4019 /// `pcm_per_channel` unchanged.
4020 #[test]
4021 fn dispatch_acpl_5x_pair_rejects_unaligned_sample_count() {
4022 let params = CodecParameters::audio(CodecId::new("ac4"));
4023 let mut dec = Ac4Decoder::new(¶ms);
4024 // 100 is not a multiple of 64.
4025 let n = 100usize;
4026 let mut pcm_per_channel: Vec<Option<Vec<i16>>> =
4027 vec![Some(vec![0_i16; n]), Some(vec![0_i16; n])];
4028 let cfg = dispatch_stub_cfg(12);
4029 let data_1 = dispatch_stub_data_1ch(0, 0, cfg.num_param_bands);
4030 let data_2 = dispatch_stub_data_1ch(0, 0, cfg.num_param_bands);
4031
4032 dec.dispatch_acpl_5x_pair(
4033 Acpl5xPairMode::AspxAcpl2,
4034 &cfg,
4035 &data_1,
4036 &data_2,
4037 n,
4038 None,
4039 None,
4040 None,
4041 &mut pcm_per_channel,
4042 );
4043
4044 // Must have left pcm_per_channel as-is (only 2 entries).
4045 assert_eq!(
4046 pcm_per_channel.len(),
4047 2,
4048 "dispatch must not grow pcm_per_channel on unaligned input"
4049 );
4050 }
4051
4052 /// When the L/R carriers are absent (slots empty), dispatch should
4053 /// still synthesise five channels using the zero-filled fallback.
4054 #[test]
4055 fn dispatch_acpl_5x_pair_zero_fills_missing_carriers() {
4056 let params = CodecParameters::audio(CodecId::new("ac4"));
4057 let mut dec = Ac4Decoder::new(¶ms);
4058 let n = 1_920usize;
4059 let mut pcm_per_channel: Vec<Option<Vec<i16>>> = vec![None, None];
4060 let cfg = dispatch_stub_cfg(9);
4061 let data_1 = dispatch_stub_data_1ch(1, 0, cfg.num_param_bands);
4062 let data_2 = dispatch_stub_data_1ch(-1, 0, cfg.num_param_bands);
4063
4064 dec.dispatch_acpl_5x_pair(
4065 Acpl5xPairMode::AspxAcpl2,
4066 &cfg,
4067 &data_1,
4068 &data_2,
4069 n,
4070 None,
4071 None,
4072 None,
4073 &mut pcm_per_channel,
4074 );
4075
4076 assert!(pcm_per_channel.len() >= 5);
4077 // With zero-filled carriers, every slot should be a length-n
4078 // i16 vector full of zeros (or near-zero from QMF prototype
4079 // ringing — the QMF banks initialise to zero history).
4080 for (ch, slot) in pcm_per_channel.iter().enumerate().take(5) {
4081 let pcm = slot.as_ref().unwrap();
4082 assert_eq!(pcm.len(), n);
4083 // Energy may be zero or near-zero from QMF startup.
4084 let max_abs = pcm.iter().map(|&s| s.unsigned_abs()).max().unwrap_or(0);
4085 assert!(
4086 max_abs < 100,
4087 "channel {ch}: zero-input synthesis should produce silence-like output, max_abs = {max_abs}"
4088 );
4089 }
4090 }
4091
4092 /// Verify the 5_X pair dispatch correctly resolves the active
4093 /// `acpl_config_1ch_*` slot via `five_x_mode`. This is a static
4094 /// regression check: the detection logic must look at
4095 /// `acpl_config_1ch_partial` for AspxAcpl1 and
4096 /// `acpl_config_1ch_full` for AspxAcpl2.
4097 #[test]
4098 fn dispatch_acpl_5x_pair_resolves_partial_for_aspx_acpl_1() {
4099 // Smoke check that compile-time dispatch reads the right tools
4100 // slot — concretely: AspxAcpl1 mode must have non-zero
4101 // qmf_band picked up from the partial config, AspxAcpl2 must
4102 // have qmf_band == 0 (full config doesn't carry it).
4103 let cfg_partial = AcplConfig1ch {
4104 num_param_bands_id: 1,
4105 num_param_bands: 12,
4106 quant_mode: AcplQuantMode::Coarse,
4107 qmf_band: 4, // PARTIAL-only field (1..8 valid)
4108 };
4109 let cfg_full = AcplConfig1ch {
4110 num_param_bands_id: 0,
4111 num_param_bands: 9,
4112 quant_mode: AcplQuantMode::Fine,
4113 qmf_band: 0, // FULL: always zero per Table 59
4114 };
4115 // Distinct field values prove the resolution path picked up
4116 // the right tools entry.
4117 assert_eq!(cfg_partial.qmf_band, 4);
4118 assert_eq!(cfg_full.qmf_band, 0);
4119 assert_ne!(cfg_partial.num_param_bands_id, cfg_full.num_param_bands_id);
4120 }
4121
4122 /// Round 37: when a real centre PCM carrier is supplied via
4123 /// `centre_pcm`, the dispatch helper must thread it through
4124 /// Pseudocode 117's `z4 = x2` passthrough — the synthesised
4125 /// centre PCM should mirror the input (not be silent like the
4126 /// round-36 zero-fill placeholder). We check that the output
4127 /// centre channel has measurable energy when fed a non-zero
4128 /// centre buffer.
4129 #[test]
4130 fn dispatch_acpl_5x_pair_centre_pcm_passthrough_emits_centre_energy() {
4131 let params = CodecParameters::audio(CodecId::new("ac4"));
4132 let mut dec = Ac4Decoder::new(¶ms);
4133 let n = 1_920usize;
4134 let carrier_l: Vec<i16> = vec![0; n];
4135 let carrier_r: Vec<i16> = vec![0; n];
4136 let mut pcm_per_channel: Vec<Option<Vec<i16>>> = vec![Some(carrier_l), Some(carrier_r)];
4137 let cfg = dispatch_stub_cfg(12);
4138 let data_1 = dispatch_stub_data_1ch(0, 0, cfg.num_param_bands);
4139 let data_2 = dispatch_stub_data_1ch(0, 0, cfg.num_param_bands);
4140 // Centre PCM as f32 — alternating ±0.05 amplitude so the QMF
4141 // analysis + synthesis round-trip lands measurable energy on
4142 // ch2 even though L/R/Ls/Rs feed silence.
4143 let centre_pcm: Vec<f32> = (0..n)
4144 .map(|i| if i & 1 == 0 { 0.05_f32 } else { -0.05_f32 })
4145 .collect();
4146
4147 dec.dispatch_acpl_5x_pair(
4148 Acpl5xPairMode::AspxAcpl2,
4149 &cfg,
4150 &data_1,
4151 &data_2,
4152 n,
4153 Some(¢re_pcm),
4154 None,
4155 None,
4156 &mut pcm_per_channel,
4157 );
4158
4159 assert!(pcm_per_channel.len() >= 5);
4160 let centre = pcm_per_channel[2]
4161 .as_ref()
4162 .expect("centre channel populated");
4163 assert_eq!(centre.len(), n);
4164 let centre_energy: u64 = centre.iter().map(|&s| s.unsigned_abs() as u64).sum();
4165 assert!(
4166 centre_energy > 0,
4167 "centre channel must carry energy from centre_pcm input"
4168 );
4169 }
4170
4171 /// Round 37: end-to-end glue test for the 7_X ACPL_2 dispatch
4172 /// path. A 7_X SIMPLE-Cfg0 substream's `mono_data(0)` centre +
4173 /// `acpl_data_1ch_pair[]` should drive Pseudocode 120 the same
4174 /// way the 5_X path drives Pseudocode 117 (modulo the additional
4175 /// channels which stay at silence for ACPL_1/_2 since the SIMPLE/
4176 /// ASPX additional-channel block isn't in scope).
4177 ///
4178 /// We only validate that `dispatch_acpl_5x_pair` accepts the same
4179 /// `Acpl5xPairMode` selectors when fed from `seven_x_mode`-derived
4180 /// state — the channel mapping core is identical. This is the
4181 /// type-level proof the 7_X dispatch wires through; the actual
4182 /// 7.0/7.1 rendering uses the same code path.
4183 #[test]
4184 fn seven_x_pair_dispatch_resolves_same_mode_as_five_x() {
4185 // Both 5_X AspxAcpl1 / AspxAcpl2 and 7_X AspxAcpl1 / AspxAcpl2
4186 // map to the same `Acpl5xPairMode` selector (the synthesis
4187 // shape is identical per Pseudocode 117 vs 120 — only the
4188 // surrounding additional-channel handling differs).
4189 let mode_5x_1 = match crate::mch::FiveXCodecMode::AspxAcpl1 {
4190 crate::mch::FiveXCodecMode::AspxAcpl1 => Acpl5xPairMode::AspxAcpl1,
4191 _ => unreachable!(),
4192 };
4193 let mode_7x_1 = match crate::mch::SevenXCodecMode::AspxAcpl1 {
4194 crate::mch::SevenXCodecMode::AspxAcpl1 => Acpl5xPairMode::AspxAcpl1,
4195 _ => unreachable!(),
4196 };
4197 assert_eq!(mode_5x_1, mode_7x_1);
4198
4199 let mode_5x_2 = match crate::mch::FiveXCodecMode::AspxAcpl2 {
4200 crate::mch::FiveXCodecMode::AspxAcpl2 => Acpl5xPairMode::AspxAcpl2,
4201 _ => unreachable!(),
4202 };
4203 let mode_7x_2 = match crate::mch::SevenXCodecMode::AspxAcpl2 {
4204 crate::mch::SevenXCodecMode::AspxAcpl2 => Acpl5xPairMode::AspxAcpl2,
4205 _ => unreachable!(),
4206 };
4207 assert_eq!(mode_5x_2, mode_7x_2);
4208 }
4209
4210 /// Round 37: `imdct_mono_lfe_data_f32` IMDCTs a `MonoLfeData`'s
4211 /// `scaled_spec` into a length-n PCM buffer. Returns `None` when
4212 /// the body wasn't decoded (LFE / SSF / Huffman miss) or when the
4213 /// signalled transform-length differs from the requested `n`.
4214 #[test]
4215 fn imdct_mono_lfe_data_f32_returns_none_when_no_scaled_spec() {
4216 let params = CodecParameters::audio(CodecId::new("ac4"));
4217 let mut dec = Ac4Decoder::new(¶ms);
4218 let mono = crate::mch::MonoLfeData {
4219 b_lfe: false,
4220 spec_frontend_bit: 0,
4221 transform_info: None,
4222 psy_info: None,
4223 scaled_spec: None,
4224 };
4225 assert!(dec.imdct_mono_lfe_data_f32(&mono, 2, 1_920).is_none());
4226 }
4227
4228 /// Round 37: when the parsed transform-length matches the frame
4229 /// length and `scaled_spec` is populated, the IMDCT helper returns
4230 /// a length-n PCM buffer (overlap-added with the slot's history).
4231 #[test]
4232 fn imdct_mono_lfe_data_f32_imdcts_when_scaled_spec_present() {
4233 let params = CodecParameters::audio(CodecId::new("ac4"));
4234 let mut dec = Ac4Decoder::new(¶ms);
4235 let mono = crate::mch::MonoLfeData {
4236 b_lfe: false,
4237 spec_frontend_bit: 0,
4238 transform_info: Some(crate::asf::AsfTransformInfo {
4239 b_long_frame: true,
4240 transf_length: [0; 2],
4241 transform_length_0: 1_920,
4242 transform_length_1: 1_920,
4243 }),
4244 psy_info: None,
4245 // All-zero spectrum — IMDCT will produce a length-1920 PCM
4246 // buffer of zeros (modulo the windowed overlap-add IIR
4247 // ringing, which starts from zero history).
4248 scaled_spec: Some(vec![0.0_f32; 1_920]),
4249 };
4250 let pcm = dec.imdct_mono_lfe_data_f32(&mono, 2, 1_920).unwrap();
4251 assert_eq!(pcm.len(), 1_920);
4252 // All-zero spectrum + zero history -> all-zero PCM.
4253 assert!(pcm.iter().all(|&s| s == 0.0));
4254 }
4255
4256 /// Round 38: `dispatch_5x_cfg2_simple_aspx` IMDCTs the parsed
4257 /// `four_channel_data.scaled_spec_per_channel[0..4]` into PCM slots
4258 /// 0/1/3/4 (L/R/Ls/Rs per Table 180) and the trailing
4259 /// `cfg2_back_mono` into slot 2 (C). With non-zero ramp spectra in
4260 /// slots 0/1/3/4 and slot 2, every channel must carry energy after
4261 /// IMDCT + overlap-add (the windowed first-frame output isn't
4262 /// pure silence even though the prior overlap history was zero).
4263 #[test]
4264 fn dispatch_5x_cfg2_populates_l_r_c_ls_rs() {
4265 let params = CodecParameters::audio(CodecId::new("ac4"));
4266 let mut dec = Ac4Decoder::new(¶ms);
4267 let n: usize = 1_920;
4268 let ti = crate::asf::AsfTransformInfo {
4269 b_long_frame: true,
4270 transf_length: [0; 2],
4271 transform_length_0: n as u32,
4272 transform_length_1: n as u32,
4273 };
4274 // Build per-channel "ramp" spectra so every output carries energy.
4275 // The amplitude is small enough that i16 quantisation doesn't
4276 // squash everything to zero after IMDCT + windowing.
4277 let mk_ramp = |bias: f32| -> Vec<f32> { (0..n).map(|i| bias + 1e-3 * i as f32).collect() };
4278 let four = crate::mch::FourChannelData {
4279 transform_info: Some(ti),
4280 psy_info: None,
4281 info: None,
4282 scaled_spec_per_channel: vec![
4283 Some(mk_ramp(0.10)),
4284 Some(mk_ramp(0.20)),
4285 Some(mk_ramp(0.30)),
4286 Some(mk_ramp(0.40)),
4287 ],
4288 };
4289 let back_mono = crate::mch::MonoLfeData {
4290 b_lfe: false,
4291 spec_frontend_bit: 0,
4292 transform_info: Some(ti),
4293 psy_info: None,
4294 scaled_spec: Some(mk_ramp(0.50)),
4295 };
4296 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 5];
4297 // No ASPX trailers (low-band only) — equivalent to round-38
4298 // SIMPLE-mode behaviour. ASPX-extended outputs are covered by
4299 // `dispatch_5x_cfg2_with_aspx_trailers_*` below.
4300 dec.dispatch_5x_cfg2_simple_aspx(
4301 &four,
4302 Some(&back_mono),
4303 None,
4304 None,
4305 None,
4306 None,
4307 None,
4308 1,
4309 n,
4310 &mut pcm,
4311 );
4312 // Every L/R/C/Ls/Rs slot must be populated and carry energy.
4313 for (slot, entry) in pcm.iter().enumerate().take(5) {
4314 let v = entry
4315 .as_ref()
4316 .unwrap_or_else(|| panic!("slot {slot} populated"));
4317 assert_eq!(v.len(), n);
4318 let energy: u64 = v.iter().map(|&s| s.unsigned_abs() as u64).sum();
4319 assert!(
4320 energy > 0,
4321 "slot {slot} must carry energy from per-channel ramp"
4322 );
4323 }
4324 }
4325
4326 /// Round 38: `dispatch_5x_cfg2_simple_aspx` is a no-op when the
4327 /// `four_channel_data.transform_info` carrier-length differs from
4328 /// the requested `samples` count — leaves all output slots unchanged.
4329 #[test]
4330 fn dispatch_5x_cfg2_noop_on_length_mismatch() {
4331 let params = CodecParameters::audio(CodecId::new("ac4"));
4332 let mut dec = Ac4Decoder::new(¶ms);
4333 let ti = crate::asf::AsfTransformInfo {
4334 b_long_frame: true,
4335 transf_length: [0; 2],
4336 transform_length_0: 1_024,
4337 transform_length_1: 1_024,
4338 };
4339 let four = crate::mch::FourChannelData {
4340 transform_info: Some(ti),
4341 psy_info: None,
4342 info: None,
4343 scaled_spec_per_channel: vec![
4344 Some(vec![0.1_f32; 1_024]),
4345 Some(vec![0.2_f32; 1_024]),
4346 Some(vec![0.3_f32; 1_024]),
4347 Some(vec![0.4_f32; 1_024]),
4348 ],
4349 };
4350 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 5];
4351 // Request a different sample count.
4352 dec.dispatch_5x_cfg2_simple_aspx(
4353 &four, None, None, None, None, None, None, 1, 1_920, &mut pcm,
4354 );
4355 for (slot, entry) in pcm.iter().enumerate().take(5) {
4356 assert!(
4357 entry.is_none(),
4358 "slot {slot} should be untouched on length mismatch"
4359 );
4360 }
4361 }
4362
4363 /// Round 41: `dispatch_5x_cfg2_simple_aspx` runs the per-channel
4364 /// A-SPX bandwidth-extension for L/R/Ls/Rs/C using the captured
4365 /// trailer state. Comparison: with `aspx_lr` + `aspx_ls_rs` +
4366 /// `aspx_centre` populated and a non-degenerate `aspx_config`,
4367 /// the front-pair / surround-pair / centre PCM differs from
4368 /// the round-38 low-band-only path on at least one slot.
4369 #[test]
4370 fn dispatch_5x_cfg2_aspx_trailers_change_output_vs_low_band_only() {
4371 // Use n_slots = 30 so the tone's QMF analysis settles and the
4372 // HF tile copy has at least one full envelope window. (This is
4373 // the same shape `aspx_extend_pcm_produces_non_silent_output`
4374 // exercises.)
4375 let n_slots = 30usize;
4376 let n = n_slots * 64;
4377 let mk_tone = |freq_hz: f32, bias: f32| -> Vec<f32> {
4378 // Spectrum-domain coefficients are arbitrary here; we just
4379 // need something that survives IMDCT + windowing without
4380 // collapsing to zero and that the ASPX path can extend.
4381 (0..n)
4382 .map(|i| bias + (2.0 * std::f32::consts::PI * freq_hz / 48_000.0 * i as f32).sin())
4383 .collect()
4384 };
4385 let ti = crate::asf::AsfTransformInfo {
4386 b_long_frame: true,
4387 transf_length: [0; 2],
4388 transform_length_0: n as u32,
4389 transform_length_1: n as u32,
4390 };
4391 let four = crate::mch::FourChannelData {
4392 transform_info: Some(ti),
4393 psy_info: None,
4394 info: None,
4395 scaled_spec_per_channel: vec![
4396 Some(mk_tone(500.0, 0.10)),
4397 Some(mk_tone(700.0, 0.20)),
4398 Some(mk_tone(900.0, 0.30)),
4399 Some(mk_tone(1100.0, 0.40)),
4400 ],
4401 };
4402 let back_mono = crate::mch::MonoLfeData {
4403 b_lfe: false,
4404 spec_frontend_bit: 0,
4405 transform_info: Some(ti),
4406 psy_info: None,
4407 scaled_spec: Some(mk_tone(1300.0, 0.50)),
4408 };
4409 // Round-38 path: no trailers -> low-band PCM only.
4410 let params = CodecParameters::audio(CodecId::new("ac4"));
4411 let mut dec_lb = Ac4Decoder::new(¶ms);
4412 let mut pcm_lb: Vec<Option<Vec<i16>>> = vec![None; 5];
4413 dec_lb.dispatch_5x_cfg2_simple_aspx(
4414 &four,
4415 Some(&back_mono),
4416 None,
4417 None,
4418 None,
4419 None,
4420 None,
4421 1,
4422 n,
4423 &mut pcm_lb,
4424 );
4425 // Round-41 path: with synthetic trailers.
4426 let cfg = aspx::AspxConfig {
4427 quant_mode_env: aspx::AspxQuantStep::Fine,
4428 start_freq: 0,
4429 stop_freq: 0,
4430 master_freq_scale: aspx::AspxMasterFreqScale::HighRes,
4431 interpolation: false,
4432 preflat: false,
4433 limiter: false,
4434 noise_sbg: 0,
4435 num_env_bits_fixfix: 0,
4436 freq_res_mode: aspx::AspxFreqResMode::Signalled,
4437 };
4438 let tables = aspx::derive_aspx_frequency_tables(&cfg, 0).unwrap();
4439 let framing = aspx::AspxFraming {
4440 int_class: aspx::AspxIntClass::FixFix,
4441 num_env: 1,
4442 num_noise: 1,
4443 freq_res: vec![true],
4444 var_bord_left: None,
4445 var_bord_right: None,
4446 num_rel_left: 0,
4447 num_rel_right: 0,
4448 rel_bord_left: vec![],
4449 rel_bord_right: vec![],
4450 tsg_ptr: None,
4451 };
4452 let mk_ch = || aspx::FiveXAspxChannelTrailer {
4453 framing: framing.clone(),
4454 qmode_env: aspx::AspxQuantStep::Fine,
4455 delta_dir: aspx::AspxDeltaDir {
4456 sig_delta_dir: vec![false],
4457 noise_delta_dir: vec![false],
4458 },
4459 // sig / noise envelopes empty: aspx_extend_pcm falls
4460 // through to the bare-tile-copy + flat envelope gain
4461 // scaffold which still produces a non-zero HF tail.
4462 data_sig: Vec::new(),
4463 data_noise: Vec::new(),
4464 add_harmonic: None,
4465 tna_mode: None,
4466 };
4467 let trailer_2ch = aspx::FiveXAspxTrailer {
4468 xover: 0,
4469 frequency_tables: tables.clone(),
4470 primary: mk_ch(),
4471 secondary: Some(mk_ch()),
4472 };
4473 let trailer_1ch = aspx::FiveXAspxTrailer {
4474 xover: 0,
4475 frequency_tables: tables,
4476 primary: mk_ch(),
4477 secondary: None,
4478 };
4479 let mut dec_aspx = Ac4Decoder::new(¶ms);
4480 let mut pcm_aspx: Vec<Option<Vec<i16>>> = vec![None; 5];
4481 dec_aspx.dispatch_5x_cfg2_simple_aspx(
4482 &four,
4483 Some(&back_mono),
4484 Some(&trailer_2ch),
4485 Some(&trailer_2ch),
4486 Some(&trailer_1ch),
4487 Some(cfg),
4488 None,
4489 1,
4490 n,
4491 &mut pcm_aspx,
4492 );
4493 // Every slot must be populated in both runs.
4494 for slot in 0..5 {
4495 assert!(pcm_lb[slot].is_some(), "low-band slot {slot} populated");
4496 assert!(pcm_aspx[slot].is_some(), "aspx slot {slot} populated");
4497 }
4498 // At least one slot's output must differ between runs (the
4499 // ASPX path adds high-band content that the low-band-only
4500 // path lacks).
4501 let mut differs = 0usize;
4502 for slot in 0..5 {
4503 let a = pcm_lb[slot].as_ref().unwrap();
4504 let b = pcm_aspx[slot].as_ref().unwrap();
4505 assert_eq!(a.len(), b.len());
4506 if a != b {
4507 differs += 1;
4508 }
4509 }
4510 assert!(
4511 differs > 0,
4512 "ASPX trailer path must produce at least one output that differs from the low-band-only path"
4513 );
4514 }
4515
4516 /// Round 39: `dispatch_5x_cfg0_simple_aspx` IMDCTs each
4517 /// `two_channel_data.scaled_spec_per_channel[0..2]` into PCM slots
4518 /// per Table 180 column 0:
4519 ///
4520 /// * `b_2ch_mode == false` (default): tcd_a -> [0,1] (L,R),
4521 /// tcd_b -> [3,4] (Ls,Rs).
4522 /// * `b_2ch_mode == true` (alternate): tcd_a -> [0,3] (L,Ls),
4523 /// tcd_b -> [1,4] (R,Rs).
4524 ///
4525 /// `cfg0_centre_mono` lands on slot 2 (C). With non-zero ramp spectra
4526 /// in every input slot every output L/R/C/Ls/Rs slot must carry
4527 /// energy after IMDCT + overlap-add.
4528 #[test]
4529 fn dispatch_5x_cfg0_populates_l_r_c_ls_rs_default_2ch_mode() {
4530 let params = CodecParameters::audio(CodecId::new("ac4"));
4531 let mut dec = Ac4Decoder::new(¶ms);
4532 let n: usize = 1_920;
4533 let ti = crate::asf::AsfTransformInfo {
4534 b_long_frame: true,
4535 transf_length: [0; 2],
4536 transform_length_0: n as u32,
4537 transform_length_1: n as u32,
4538 };
4539 let mk_ramp = |bias: f32| -> Vec<f32> { (0..n).map(|i| bias + 1e-3 * i as f32).collect() };
4540 let tcd_a = crate::mch::TwoChannelData {
4541 transform_info: Some(ti),
4542 psy_info: None,
4543 chparam: None,
4544 scaled_spec_per_channel: vec![Some(mk_ramp(0.10)), Some(mk_ramp(0.20))],
4545 };
4546 let tcd_b = crate::mch::TwoChannelData {
4547 transform_info: Some(ti),
4548 psy_info: None,
4549 chparam: None,
4550 scaled_spec_per_channel: vec![Some(mk_ramp(0.30)), Some(mk_ramp(0.40))],
4551 };
4552 let centre = crate::mch::MonoLfeData {
4553 b_lfe: false,
4554 spec_frontend_bit: 0,
4555 transform_info: Some(ti),
4556 psy_info: None,
4557 scaled_spec: Some(mk_ramp(0.50)),
4558 };
4559 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 5];
4560 dec.dispatch_5x_cfg0_simple_aspx(
4561 &tcd_a,
4562 &tcd_b,
4563 false,
4564 Some(¢re),
4565 None,
4566 None,
4567 None,
4568 None,
4569 None,
4570 1,
4571 n,
4572 &mut pcm,
4573 );
4574 for (slot, entry) in pcm.iter().enumerate().take(5) {
4575 let v = entry
4576 .as_ref()
4577 .unwrap_or_else(|| panic!("slot {slot} populated"));
4578 assert_eq!(v.len(), n);
4579 let energy: u64 = v.iter().map(|&s| s.unsigned_abs() as u64).sum();
4580 assert!(energy > 0, "slot {slot} must carry energy from cfg0 ramp");
4581 }
4582 }
4583
4584 /// Round 39: `dispatch_5x_cfg0_simple_aspx` with `b_2ch_mode == true`
4585 /// uses the alternate Table 180 column 0b mapping: tcd_a -> [0,3],
4586 /// tcd_b -> [1,4]. The centre mono still lands on slot 2.
4587 #[test]
4588 fn dispatch_5x_cfg0_alternate_2ch_mode_maps_to_l_ls_r_rs() {
4589 let params = CodecParameters::audio(CodecId::new("ac4"));
4590 let mut dec = Ac4Decoder::new(¶ms);
4591 let n: usize = 1_920;
4592 let ti = crate::asf::AsfTransformInfo {
4593 b_long_frame: true,
4594 transf_length: [0; 2],
4595 transform_length_0: n as u32,
4596 transform_length_1: n as u32,
4597 };
4598 let mk_ramp = |bias: f32| -> Vec<f32> { (0..n).map(|i| bias + 1e-3 * i as f32).collect() };
4599 let tcd_a = crate::mch::TwoChannelData {
4600 transform_info: Some(ti),
4601 psy_info: None,
4602 chparam: None,
4603 scaled_spec_per_channel: vec![Some(mk_ramp(0.10)), Some(mk_ramp(0.20))],
4604 };
4605 let tcd_b = crate::mch::TwoChannelData {
4606 transform_info: Some(ti),
4607 psy_info: None,
4608 chparam: None,
4609 scaled_spec_per_channel: vec![Some(mk_ramp(0.30)), Some(mk_ramp(0.40))],
4610 };
4611 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 5];
4612 // No centre — slot 2 stays None.
4613 dec.dispatch_5x_cfg0_simple_aspx(
4614 &tcd_a, &tcd_b, true, None, None, None, None, None, None, 1, n, &mut pcm,
4615 );
4616 for slot in [0_usize, 1, 3, 4] {
4617 assert!(
4618 pcm[slot].as_ref().is_some(),
4619 "slot {slot} must be populated under 2ch_mode=true"
4620 );
4621 }
4622 assert!(
4623 pcm[2].is_none(),
4624 "slot 2 (C) stays untouched without centre_mono"
4625 );
4626 }
4627
4628 /// Round 39: `dispatch_5x_cfg1_simple_aspx` IMDCTs
4629 /// `three_channel_data[0..3]` into slots 0/1/2 (L/R/C) and
4630 /// `two_channel_data[0..2]` into slots 3/4 (Ls/Rs) per Table 180
4631 /// column 1.
4632 #[test]
4633 fn dispatch_5x_cfg1_populates_l_r_c_ls_rs() {
4634 let params = CodecParameters::audio(CodecId::new("ac4"));
4635 let mut dec = Ac4Decoder::new(¶ms);
4636 let n: usize = 1_920;
4637 let ti = crate::asf::AsfTransformInfo {
4638 b_long_frame: true,
4639 transf_length: [0; 2],
4640 transform_length_0: n as u32,
4641 transform_length_1: n as u32,
4642 };
4643 let mk_ramp = |bias: f32| -> Vec<f32> { (0..n).map(|i| bias + 1e-3 * i as f32).collect() };
4644 let three = crate::mch::ThreeChannelData {
4645 transform_info: Some(ti),
4646 psy_info: None,
4647 info: None,
4648 scaled_spec_per_channel: vec![
4649 Some(mk_ramp(0.10)),
4650 Some(mk_ramp(0.20)),
4651 Some(mk_ramp(0.30)),
4652 ],
4653 };
4654 let tcd = crate::mch::TwoChannelData {
4655 transform_info: Some(ti),
4656 psy_info: None,
4657 chparam: None,
4658 scaled_spec_per_channel: vec![Some(mk_ramp(0.40)), Some(mk_ramp(0.50))],
4659 };
4660 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 5];
4661 dec.dispatch_5x_cfg1_simple_aspx(
4662 &three, &tcd, None, None, None, None, None, 1, n, &mut pcm,
4663 );
4664 for (slot, entry) in pcm.iter().enumerate().take(5) {
4665 let v = entry
4666 .as_ref()
4667 .unwrap_or_else(|| panic!("slot {slot} populated"));
4668 assert_eq!(v.len(), n);
4669 let energy: u64 = v.iter().map(|&s| s.unsigned_abs() as u64).sum();
4670 assert!(energy > 0, "slot {slot} must carry energy from cfg1 ramp");
4671 }
4672 }
4673
4674 /// Round 39: `dispatch_5x_cfg3_simple_aspx` IMDCTs
4675 /// `five_channel_data[0..5]` into slots 0..4 (L/R/C/Ls/Rs) per
4676 /// Table 180 column 3.
4677 #[test]
4678 fn dispatch_5x_cfg3_populates_l_r_c_ls_rs() {
4679 let params = CodecParameters::audio(CodecId::new("ac4"));
4680 let mut dec = Ac4Decoder::new(¶ms);
4681 let n: usize = 1_920;
4682 let ti = crate::asf::AsfTransformInfo {
4683 b_long_frame: true,
4684 transf_length: [0; 2],
4685 transform_length_0: n as u32,
4686 transform_length_1: n as u32,
4687 };
4688 let mk_ramp = |bias: f32| -> Vec<f32> { (0..n).map(|i| bias + 1e-3 * i as f32).collect() };
4689 let five = crate::mch::FiveChannelData {
4690 transform_info: Some(ti),
4691 psy_info: None,
4692 info: None,
4693 scaled_spec_per_channel: vec![
4694 Some(mk_ramp(0.10)),
4695 Some(mk_ramp(0.20)),
4696 Some(mk_ramp(0.30)),
4697 Some(mk_ramp(0.40)),
4698 Some(mk_ramp(0.50)),
4699 ],
4700 };
4701 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 5];
4702 dec.dispatch_5x_cfg3_simple_aspx(&five, None, None, None, None, None, 1, n, &mut pcm);
4703 for (slot, entry) in pcm.iter().enumerate().take(5) {
4704 let v = entry
4705 .as_ref()
4706 .unwrap_or_else(|| panic!("slot {slot} populated"));
4707 assert_eq!(v.len(), n);
4708 let energy: u64 = v.iter().map(|&s| s.unsigned_abs() as u64).sum();
4709 assert!(energy > 0, "slot {slot} must carry energy from cfg3 ramp");
4710 }
4711 }
4712
4713 /// Round 39: cfg0 / cfg1 / cfg3 dispatch helpers must be no-ops on
4714 /// transform-length / sample-count mismatch — leave every output
4715 /// slot untouched.
4716 #[test]
4717 fn dispatch_5x_cfg013_noop_on_length_mismatch() {
4718 let params = CodecParameters::audio(CodecId::new("ac4"));
4719 let mut dec = Ac4Decoder::new(¶ms);
4720 let ti_short = crate::asf::AsfTransformInfo {
4721 b_long_frame: true,
4722 transf_length: [0; 2],
4723 transform_length_0: 1_024,
4724 transform_length_1: 1_024,
4725 };
4726 // cfg0
4727 let tcd = crate::mch::TwoChannelData {
4728 transform_info: Some(ti_short),
4729 psy_info: None,
4730 chparam: None,
4731 scaled_spec_per_channel: vec![Some(vec![0.1; 1_024]), Some(vec![0.2; 1_024])],
4732 };
4733 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 5];
4734 dec.dispatch_5x_cfg0_simple_aspx(
4735 &tcd, &tcd, false, None, None, None, None, None, None, 1, 1_920, &mut pcm,
4736 );
4737 assert!(pcm.iter().all(|p| p.is_none()), "cfg0 mismatch -> no-op");
4738 // cfg1
4739 let three = crate::mch::ThreeChannelData {
4740 transform_info: Some(ti_short),
4741 psy_info: None,
4742 info: None,
4743 scaled_spec_per_channel: vec![
4744 Some(vec![0.1; 1_024]),
4745 Some(vec![0.2; 1_024]),
4746 Some(vec![0.3; 1_024]),
4747 ],
4748 };
4749 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 5];
4750 dec.dispatch_5x_cfg1_simple_aspx(
4751 &three, &tcd, None, None, None, None, None, 1, 1_920, &mut pcm,
4752 );
4753 assert!(pcm.iter().all(|p| p.is_none()), "cfg1 mismatch -> no-op");
4754 // cfg3
4755 let five = crate::mch::FiveChannelData {
4756 transform_info: Some(ti_short),
4757 psy_info: None,
4758 info: None,
4759 scaled_spec_per_channel: vec![
4760 Some(vec![0.1; 1_024]),
4761 Some(vec![0.2; 1_024]),
4762 Some(vec![0.3; 1_024]),
4763 Some(vec![0.4; 1_024]),
4764 Some(vec![0.5; 1_024]),
4765 ],
4766 };
4767 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 5];
4768 dec.dispatch_5x_cfg3_simple_aspx(&five, None, None, None, None, None, 1, 1_920, &mut pcm);
4769 assert!(pcm.iter().all(|p| p.is_none()), "cfg3 mismatch -> no-op");
4770 }
4771
4772 /// Round 42: `dispatch_5x_cfg{0,1,3}_simple_aspx` honour captured
4773 /// ASPX trailers + companding flags. With non-degenerate trailers
4774 /// plus non-degenerate config, every cfg's output PCM differs
4775 /// from the round-39 low-band-only path on at least one slot,
4776 /// proving the trailer-aware ASPX extension fires.
4777 #[test]
4778 fn dispatch_5x_cfg013_with_aspx_trailers_changes_output() {
4779 // Same setup shape as `dispatch_5x_cfg2_aspx_trailers_change_output_vs_low_band_only`.
4780 let n_slots = 30usize;
4781 let n = n_slots * 64;
4782 let mk_tone = |freq_hz: f32, bias: f32| -> Vec<f32> {
4783 (0..n)
4784 .map(|i| bias + (2.0 * std::f32::consts::PI * freq_hz / 48_000.0 * i as f32).sin())
4785 .collect()
4786 };
4787 let ti = crate::asf::AsfTransformInfo {
4788 b_long_frame: true,
4789 transf_length: [0; 2],
4790 transform_length_0: n as u32,
4791 transform_length_1: n as u32,
4792 };
4793 let cfg = aspx::AspxConfig {
4794 quant_mode_env: aspx::AspxQuantStep::Fine,
4795 start_freq: 0,
4796 stop_freq: 0,
4797 master_freq_scale: aspx::AspxMasterFreqScale::HighRes,
4798 interpolation: false,
4799 preflat: false,
4800 limiter: false,
4801 noise_sbg: 0,
4802 num_env_bits_fixfix: 0,
4803 freq_res_mode: aspx::AspxFreqResMode::Signalled,
4804 };
4805 let tables = aspx::derive_aspx_frequency_tables(&cfg, 0).unwrap();
4806 let framing = aspx::AspxFraming {
4807 int_class: aspx::AspxIntClass::FixFix,
4808 num_env: 1,
4809 num_noise: 1,
4810 freq_res: vec![true],
4811 var_bord_left: None,
4812 var_bord_right: None,
4813 num_rel_left: 0,
4814 num_rel_right: 0,
4815 rel_bord_left: vec![],
4816 rel_bord_right: vec![],
4817 tsg_ptr: None,
4818 };
4819 let mk_ch = || aspx::FiveXAspxChannelTrailer {
4820 framing: framing.clone(),
4821 qmode_env: aspx::AspxQuantStep::Fine,
4822 delta_dir: aspx::AspxDeltaDir {
4823 sig_delta_dir: vec![false],
4824 noise_delta_dir: vec![false],
4825 },
4826 data_sig: Vec::new(),
4827 data_noise: Vec::new(),
4828 add_harmonic: None,
4829 tna_mode: None,
4830 };
4831 let trailer_2ch = aspx::FiveXAspxTrailer {
4832 xover: 0,
4833 frequency_tables: tables.clone(),
4834 primary: mk_ch(),
4835 secondary: Some(mk_ch()),
4836 };
4837 let trailer_1ch = aspx::FiveXAspxTrailer {
4838 xover: 0,
4839 frequency_tables: tables,
4840 primary: mk_ch(),
4841 secondary: None,
4842 };
4843 let params = CodecParameters::audio(CodecId::new("ac4"));
4844
4845 // ===== cfg0 =====
4846 let tcd_a = crate::mch::TwoChannelData {
4847 transform_info: Some(ti),
4848 psy_info: None,
4849 chparam: None,
4850 scaled_spec_per_channel: vec![Some(mk_tone(500.0, 0.10)), Some(mk_tone(700.0, 0.20))],
4851 };
4852 let tcd_b = crate::mch::TwoChannelData {
4853 transform_info: Some(ti),
4854 psy_info: None,
4855 chparam: None,
4856 scaled_spec_per_channel: vec![Some(mk_tone(900.0, 0.30)), Some(mk_tone(1100.0, 0.40))],
4857 };
4858 let centre = crate::mch::MonoLfeData {
4859 b_lfe: false,
4860 spec_frontend_bit: 0,
4861 transform_info: Some(ti),
4862 psy_info: None,
4863 scaled_spec: Some(mk_tone(1300.0, 0.50)),
4864 };
4865 let mut dec_lb = Ac4Decoder::new(¶ms);
4866 let mut pcm_lb: Vec<Option<Vec<i16>>> = vec![None; 5];
4867 dec_lb.dispatch_5x_cfg0_simple_aspx(
4868 &tcd_a,
4869 &tcd_b,
4870 false,
4871 Some(¢re),
4872 None,
4873 None,
4874 None,
4875 None,
4876 None,
4877 1,
4878 n,
4879 &mut pcm_lb,
4880 );
4881 let mut dec_aspx = Ac4Decoder::new(¶ms);
4882 let mut pcm_aspx: Vec<Option<Vec<i16>>> = vec![None; 5];
4883 dec_aspx.dispatch_5x_cfg0_simple_aspx(
4884 &tcd_a,
4885 &tcd_b,
4886 false,
4887 Some(¢re),
4888 Some(&trailer_2ch),
4889 Some(&trailer_2ch),
4890 Some(&trailer_1ch),
4891 Some(cfg),
4892 None,
4893 1,
4894 n,
4895 &mut pcm_aspx,
4896 );
4897 let mut differs = 0usize;
4898 for slot in 0..5 {
4899 let a = pcm_lb[slot].as_ref().unwrap();
4900 let b = pcm_aspx[slot].as_ref().unwrap();
4901 if a != b {
4902 differs += 1;
4903 }
4904 }
4905 assert!(
4906 differs > 0,
4907 "cfg0 ASPX trailers must change output vs low-band-only"
4908 );
4909
4910 // ===== cfg1 =====
4911 let three = crate::mch::ThreeChannelData {
4912 transform_info: Some(ti),
4913 psy_info: None,
4914 info: None,
4915 scaled_spec_per_channel: vec![
4916 Some(mk_tone(500.0, 0.10)),
4917 Some(mk_tone(700.0, 0.20)),
4918 Some(mk_tone(900.0, 0.30)),
4919 ],
4920 };
4921 let tcd = crate::mch::TwoChannelData {
4922 transform_info: Some(ti),
4923 psy_info: None,
4924 chparam: None,
4925 scaled_spec_per_channel: vec![Some(mk_tone(1100.0, 0.40)), Some(mk_tone(1300.0, 0.50))],
4926 };
4927 let mut dec_lb = Ac4Decoder::new(¶ms);
4928 let mut pcm_lb: Vec<Option<Vec<i16>>> = vec![None; 5];
4929 dec_lb.dispatch_5x_cfg1_simple_aspx(
4930 &three,
4931 &tcd,
4932 None,
4933 None,
4934 None,
4935 None,
4936 None,
4937 1,
4938 n,
4939 &mut pcm_lb,
4940 );
4941 let mut dec_aspx = Ac4Decoder::new(¶ms);
4942 let mut pcm_aspx: Vec<Option<Vec<i16>>> = vec![None; 5];
4943 dec_aspx.dispatch_5x_cfg1_simple_aspx(
4944 &three,
4945 &tcd,
4946 Some(&trailer_2ch),
4947 Some(&trailer_2ch),
4948 Some(&trailer_1ch),
4949 Some(cfg),
4950 None,
4951 1,
4952 n,
4953 &mut pcm_aspx,
4954 );
4955 let mut differs = 0usize;
4956 for slot in 0..5 {
4957 let a = pcm_lb[slot].as_ref().unwrap();
4958 let b = pcm_aspx[slot].as_ref().unwrap();
4959 if a != b {
4960 differs += 1;
4961 }
4962 }
4963 assert!(
4964 differs > 0,
4965 "cfg1 ASPX trailers must change output vs low-band-only"
4966 );
4967
4968 // ===== cfg3 =====
4969 let five = crate::mch::FiveChannelData {
4970 transform_info: Some(ti),
4971 psy_info: None,
4972 info: None,
4973 scaled_spec_per_channel: vec![
4974 Some(mk_tone(500.0, 0.10)),
4975 Some(mk_tone(700.0, 0.20)),
4976 Some(mk_tone(900.0, 0.30)),
4977 Some(mk_tone(1100.0, 0.40)),
4978 Some(mk_tone(1300.0, 0.50)),
4979 ],
4980 };
4981 let mut dec_lb = Ac4Decoder::new(¶ms);
4982 let mut pcm_lb: Vec<Option<Vec<i16>>> = vec![None; 5];
4983 dec_lb.dispatch_5x_cfg3_simple_aspx(&five, None, None, None, None, None, 1, n, &mut pcm_lb);
4984 let mut dec_aspx = Ac4Decoder::new(¶ms);
4985 let mut pcm_aspx: Vec<Option<Vec<i16>>> = vec![None; 5];
4986 dec_aspx.dispatch_5x_cfg3_simple_aspx(
4987 &five,
4988 Some(&trailer_2ch),
4989 Some(&trailer_2ch),
4990 Some(&trailer_1ch),
4991 Some(cfg),
4992 None,
4993 1,
4994 n,
4995 &mut pcm_aspx,
4996 );
4997 let mut differs = 0usize;
4998 for slot in 0..5 {
4999 let a = pcm_lb[slot].as_ref().unwrap();
5000 let b = pcm_aspx[slot].as_ref().unwrap();
5001 if a != b {
5002 differs += 1;
5003 }
5004 }
5005 assert!(
5006 differs > 0,
5007 "cfg3 ASPX trailers must change output vs low-band-only"
5008 );
5009 }
5010
5011 /// Round 42: `five_x_compand_on_for_slot` resolves per-channel
5012 /// flags from `companding_control(num_chan)`. Verify the three
5013 /// branches: sync_flag == None (mono), sync_flag == Some(false)
5014 /// (per-channel), sync_flag == Some(true) (broadcast slot 0).
5015 #[test]
5016 fn five_x_compand_on_for_slot_resolves_each_branch() {
5017 // No CC -> always false.
5018 assert!(!Ac4Decoder::five_x_compand_on_for_slot(None, 0));
5019 assert!(!Ac4Decoder::five_x_compand_on_for_slot(None, 4));
5020
5021 // Mono (sync_flag = None, single entry).
5022 let cc_mono = aspx::CompandingControl {
5023 sync_flag: None,
5024 compand_on: vec![true],
5025 compand_avg: None,
5026 };
5027 assert!(Ac4Decoder::five_x_compand_on_for_slot(Some(&cc_mono), 0));
5028 // Out-of-range -> false (the unprocessed branch).
5029 assert!(!Ac4Decoder::five_x_compand_on_for_slot(Some(&cc_mono), 1));
5030
5031 // Per-channel (sync_flag = Some(false), 5 entries for 5_X).
5032 let cc_per = aspx::CompandingControl {
5033 sync_flag: Some(false),
5034 compand_on: vec![true, false, true, false, true],
5035 compand_avg: Some(false),
5036 };
5037 assert!(Ac4Decoder::five_x_compand_on_for_slot(Some(&cc_per), 0));
5038 assert!(!Ac4Decoder::five_x_compand_on_for_slot(Some(&cc_per), 1));
5039 assert!(Ac4Decoder::five_x_compand_on_for_slot(Some(&cc_per), 2));
5040 assert!(!Ac4Decoder::five_x_compand_on_for_slot(Some(&cc_per), 3));
5041 assert!(Ac4Decoder::five_x_compand_on_for_slot(Some(&cc_per), 4));
5042
5043 // Sync (sync_flag = Some(true), single entry mirrors all
5044 // channels).
5045 let cc_sync_on = aspx::CompandingControl {
5046 sync_flag: Some(true),
5047 compand_on: vec![true],
5048 compand_avg: None,
5049 };
5050 for slot in 0..5 {
5051 assert!(Ac4Decoder::five_x_compand_on_for_slot(
5052 Some(&cc_sync_on),
5053 slot
5054 ));
5055 }
5056 let cc_sync_off = aspx::CompandingControl {
5057 sync_flag: Some(true),
5058 compand_on: vec![false],
5059 compand_avg: Some(false),
5060 };
5061 for slot in 0..5 {
5062 assert!(!Ac4Decoder::five_x_compand_on_for_slot(
5063 Some(&cc_sync_off),
5064 slot
5065 ));
5066 }
5067 }
5068
5069 /// Round 42: `aspx_extend_pcm` with `compand_on == true` produces
5070 /// output that differs from the `compand_on == false` baseline.
5071 /// The companding gain is `g(ts) * G` per slot, where g is a
5072 /// per-slot energy power; non-trivial signal energy + non-zero
5073 /// compand_on must alter the QMF synthesis output.
5074 #[test]
5075 fn aspx_extend_pcm_with_companding_diverges_from_baseline() {
5076 let n_slots = 30usize;
5077 let n = n_slots * 64;
5078 let mut pcm = vec![0.0f32; n];
5079 let f = 800.0_f32 / 48_000.0_f32;
5080 for (i, s) in pcm.iter_mut().enumerate() {
5081 *s = (2.0 * std::f32::consts::PI * f * i as f32).sin();
5082 }
5083 let cfg = aspx::AspxConfig {
5084 quant_mode_env: aspx::AspxQuantStep::Fine,
5085 start_freq: 0,
5086 stop_freq: 0,
5087 master_freq_scale: aspx::AspxMasterFreqScale::HighRes,
5088 interpolation: false,
5089 preflat: false,
5090 limiter: false,
5091 noise_sbg: 0,
5092 num_env_bits_fixfix: 0,
5093 freq_res_mode: aspx::AspxFreqResMode::Signalled,
5094 };
5095 let tables = aspx::derive_aspx_frequency_tables(&cfg, 0).unwrap();
5096 let mut state_off = aspx::AspxChannelExtState::new();
5097 let out_off = Ac4Decoder::aspx_extend_pcm(
5098 &pcm,
5099 &tables,
5100 &cfg,
5101 None,
5102 None,
5103 None,
5104 None,
5105 None,
5106 None,
5107 None,
5108 &mut state_off,
5109 1,
5110 aspx::CompandingMode::Off,
5111 None,
5112 );
5113 let mut state_on = aspx::AspxChannelExtState::new();
5114 let out_on = Ac4Decoder::aspx_extend_pcm(
5115 &pcm,
5116 &tables,
5117 &cfg,
5118 None,
5119 None,
5120 None,
5121 None,
5122 None,
5123 None,
5124 None,
5125 &mut state_on,
5126 1,
5127 aspx::CompandingMode::PerSlot,
5128 None,
5129 );
5130 assert_eq!(out_off.len(), out_on.len());
5131 let start = 1200usize;
5132 let mut diffs = 0usize;
5133 for (a, b) in out_off[start..].iter().zip(out_on[start..].iter()) {
5134 if (a - b).abs() > 1e-6 {
5135 diffs += 1;
5136 }
5137 }
5138 assert!(
5139 diffs > (out_off.len() - start) / 4,
5140 "companding=on must alter the QMF-synthesis output (diffs={diffs})"
5141 );
5142 }
5143
5144 /// Round 42: `apply_companding_on_qmf` is a no-op when sbz <= sbx
5145 /// (degenerate band) — it must not panic on edge cases, and must
5146 /// leave the QMF matrix untouched.
5147 #[test]
5148 fn apply_companding_on_qmf_noop_on_empty_band() {
5149 let mut q = vec![vec![(1.0_f32, 1.0_f32); 16]; 64];
5150 let q_orig = q.clone();
5151 // sbx == sbz: no affected band.
5152 aspx::apply_companding_on_qmf(&mut q, 32, 32);
5153 assert_eq!(q, q_orig);
5154 // sbz < sbx: no-op.
5155 aspx::apply_companding_on_qmf(&mut q, 40, 32);
5156 assert_eq!(q, q_orig);
5157 }
5158
5159 /// Round 42: `apply_companding_on_qmf` produces unit-gain output
5160 /// on a pure-zero matrix (the `l == 0` early-return branch).
5161 #[test]
5162 fn apply_companding_on_qmf_unit_gain_on_zero_signal() {
5163 let mut q = vec![vec![(0.0_f32, 0.0_f32); 16]; 64];
5164 // All zeros + sbx=2, sbz=10: every slot's L_ch == 0 -> g = 1
5165 // -> Q stays at zero (no NaN / inf).
5166 aspx::apply_companding_on_qmf(&mut q, 2, 10);
5167 for row in q.iter() {
5168 for (re, im) in row.iter() {
5169 assert_eq!(*re, 0.0);
5170 assert_eq!(*im, 0.0);
5171 }
5172 }
5173 }
5174
5175 /// Round 39: `dispatch_7x_additional_channel_pair` IMDCTs
5176 /// `seven_x_additional_channel_data.scaled_spec_per_channel[0..2]`
5177 /// into PCM slots 5 / 6 (the F / G preliminary outputs per Table 182).
5178 /// SAP companding is the identity for now (b_use_sap_add_ch == false).
5179 #[test]
5180 fn dispatch_7x_additional_pair_populates_slots_5_and_6() {
5181 let params = CodecParameters::audio(CodecId::new("ac4"));
5182 let mut dec = Ac4Decoder::new(¶ms);
5183 let n: usize = 1_920;
5184 let ti = crate::asf::AsfTransformInfo {
5185 b_long_frame: true,
5186 transf_length: [0; 2],
5187 transform_length_0: n as u32,
5188 transform_length_1: n as u32,
5189 };
5190 let mk_ramp = |bias: f32| -> Vec<f32> { (0..n).map(|i| bias + 1e-3 * i as f32).collect() };
5191 let add = crate::mch::TwoChannelData {
5192 transform_info: Some(ti),
5193 psy_info: None,
5194 chparam: None,
5195 scaled_spec_per_channel: vec![Some(mk_ramp(0.30)), Some(mk_ramp(0.40))],
5196 };
5197 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 5];
5198 dec.dispatch_7x_additional_channel_pair(&add, None, [3, 4], None, n, &mut pcm);
5199 // Slots 0..4 untouched, slots 5/6 populated.
5200 for (slot, entry) in pcm.iter().enumerate().take(5) {
5201 assert!(entry.is_none(), "slot {slot} stays untouched");
5202 }
5203 assert_eq!(pcm.len(), 7);
5204 for slot in [5_usize, 6] {
5205 let v = pcm[slot]
5206 .as_ref()
5207 .unwrap_or_else(|| panic!("slot {slot} populated"));
5208 assert_eq!(v.len(), n);
5209 let energy: u64 = v.iter().map(|&s| s.unsigned_abs() as u64).sum();
5210 assert!(energy > 0, "slot {slot} must carry F/G energy");
5211 }
5212 }
5213
5214 /// Round 39: `dispatch_7x_additional_channel_pair` is a no-op when
5215 /// the carrier-length differs from the requested sample count.
5216 #[test]
5217 fn dispatch_7x_additional_pair_noop_on_length_mismatch() {
5218 let params = CodecParameters::audio(CodecId::new("ac4"));
5219 let mut dec = Ac4Decoder::new(¶ms);
5220 let ti = crate::asf::AsfTransformInfo {
5221 b_long_frame: true,
5222 transf_length: [0; 2],
5223 transform_length_0: 1_024,
5224 transform_length_1: 1_024,
5225 };
5226 let add = crate::mch::TwoChannelData {
5227 transform_info: Some(ti),
5228 psy_info: None,
5229 chparam: None,
5230 scaled_spec_per_channel: vec![Some(vec![0.1; 1_024]), Some(vec![0.2; 1_024])],
5231 };
5232 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 7];
5233 dec.dispatch_7x_additional_channel_pair(&add, None, [3, 4], None, 1_920, &mut pcm);
5234 for (slot, entry) in pcm.iter().enumerate() {
5235 assert!(
5236 entry.is_none(),
5237 "slot {slot} should be untouched on length mismatch"
5238 );
5239 }
5240 }
5241
5242 /// Round 40: with SAP `b_use_sap_add_ch == true` and identity
5243 /// chparam_info coefficients (sap_mode = 0 -> a=d=1, b=c=0), the
5244 /// dispatch should emit the partner spectrum on the partner slot
5245 /// and zero on the additional pair slot (since c=0, d=1 means
5246 /// `out_low = 0*P + 1*F = F`; identity passes F through to slot 5/6
5247 /// and P unchanged to partner slot — equivalent to the no-SAP path
5248 /// but with the partner slot also explicitly populated from the
5249 /// shared spectrum).
5250 #[test]
5251 fn dispatch_7x_additional_pair_sap_identity_routes_partner_and_additional() {
5252 let params = CodecParameters::audio(CodecId::new("ac4"));
5253 let mut dec = Ac4Decoder::new(¶ms);
5254 let n: usize = 1_920;
5255 let ti = crate::asf::AsfTransformInfo {
5256 b_long_frame: true,
5257 transf_length: [0; 2],
5258 transform_length_0: n as u32,
5259 transform_length_1: n as u32,
5260 };
5261 let mk_ramp = |bias: f32| -> Vec<f32> { (0..n).map(|i| bias + 1e-3 * i as f32).collect() };
5262 let add = crate::mch::TwoChannelData {
5263 transform_info: Some(ti),
5264 psy_info: None,
5265 chparam: None,
5266 scaled_spec_per_channel: vec![Some(mk_ramp(0.30)), Some(mk_ramp(0.40))],
5267 };
5268 let partner_d = mk_ramp(0.10);
5269 let partner_e = mk_ramp(0.20);
5270 let chparam = [
5271 crate::asf::ChparamInfo::default(),
5272 crate::asf::ChparamInfo::default(),
5273 ];
5274 let mut pcm: Vec<Option<Vec<i16>>> = vec![None; 5];
5275 dec.dispatch_7x_additional_channel_pair(
5276 &add,
5277 Some([partner_d.as_slice(), partner_e.as_slice()]),
5278 [3, 4],
5279 Some(&chparam),
5280 n,
5281 &mut pcm,
5282 );
5283 assert!(pcm.len() >= 7);
5284 // Partner slots 3/4 should now carry P (from the IMDCT of
5285 // partner_d / partner_e) — non-zero energy.
5286 for slot in [3_usize, 4] {
5287 let v = pcm[slot]
5288 .as_ref()
5289 .unwrap_or_else(|| panic!("partner slot {slot} populated"));
5290 assert_eq!(v.len(), n);
5291 }
5292 // Additional pair slots 5/6 carry F/G via the identity SAP
5293 // (out_low = 0*P + 1*F = F).
5294 for slot in [5_usize, 6] {
5295 let v = pcm[slot]
5296 .as_ref()
5297 .unwrap_or_else(|| panic!("add slot {slot} populated"));
5298 assert_eq!(v.len(), n);
5299 let energy: u64 = v.iter().map(|&s| s.unsigned_abs() as u64).sum();
5300 assert!(energy > 0, "add slot {slot} must carry F/G energy");
5301 }
5302 }
5303
5304 /// Round 43: `five_x_compand_mode_for_slot` resolves the active
5305 /// branch of Pseudocode 121 per output channel. Verify each of
5306 /// the (sync, on, avg) combinations the spec admits.
5307 #[test]
5308 fn five_x_compand_mode_for_slot_resolves_each_branch() {
5309 // None CC -> Off everywhere.
5310 for slot in 0..5 {
5311 assert_eq!(
5312 Ac4Decoder::five_x_compand_mode_for_slot(None, slot),
5313 aspx::CompandingMode::Off
5314 );
5315 }
5316 // Per-channel mix: ch0 on, ch1 off+avg, ch2 off (no avg).
5317 let cc_per = aspx::CompandingControl {
5318 sync_flag: Some(false),
5319 compand_on: vec![true, false, false, true, true],
5320 compand_avg: Some(true),
5321 };
5322 assert_eq!(
5323 Ac4Decoder::five_x_compand_mode_for_slot(Some(&cc_per), 0),
5324 aspx::CompandingMode::PerSlot
5325 );
5326 assert_eq!(
5327 Ac4Decoder::five_x_compand_mode_for_slot(Some(&cc_per), 1),
5328 aspx::CompandingMode::Averaged
5329 );
5330 // Sync per-slot.
5331 let cc_sync_on = aspx::CompandingControl {
5332 sync_flag: Some(true),
5333 compand_on: vec![true],
5334 compand_avg: None,
5335 };
5336 for slot in 0..5 {
5337 assert_eq!(
5338 Ac4Decoder::five_x_compand_mode_for_slot(Some(&cc_sync_on), slot),
5339 aspx::CompandingMode::SyncPerSlot
5340 );
5341 }
5342 // Sync averaged.
5343 let cc_sync_avg = aspx::CompandingControl {
5344 sync_flag: Some(true),
5345 compand_on: vec![false],
5346 compand_avg: Some(true),
5347 };
5348 for slot in 0..5 {
5349 assert_eq!(
5350 Ac4Decoder::five_x_compand_mode_for_slot(Some(&cc_sync_avg), slot),
5351 aspx::CompandingMode::SyncAveraged
5352 );
5353 }
5354 }
5355
5356 /// Round 43: `aspx_extend_pcm` honours the sb0 override — passing
5357 /// a non-default sb0 (the ASPX_ACPL_1 `acpl_qmf_band` rule)
5358 /// produces output that differs from the default `tables.sbx`
5359 /// baseline.
5360 #[test]
5361 fn aspx_extend_pcm_with_sb0_override_changes_output() {
5362 let n_slots = 30usize;
5363 let n = n_slots * 64;
5364 let mut pcm = vec![0.0f32; n];
5365 let f = 1200.0_f32 / 48_000.0_f32;
5366 for (i, s) in pcm.iter_mut().enumerate() {
5367 *s = (2.0 * std::f32::consts::PI * f * i as f32).sin();
5368 }
5369 let cfg = aspx::AspxConfig {
5370 quant_mode_env: aspx::AspxQuantStep::Fine,
5371 start_freq: 0,
5372 stop_freq: 0,
5373 master_freq_scale: aspx::AspxMasterFreqScale::HighRes,
5374 interpolation: false,
5375 preflat: false,
5376 limiter: false,
5377 noise_sbg: 0,
5378 num_env_bits_fixfix: 0,
5379 freq_res_mode: aspx::AspxFreqResMode::Signalled,
5380 };
5381 let tables = aspx::derive_aspx_frequency_tables(&cfg, 0).unwrap();
5382 // Default sb0 (== tables.sbx).
5383 let mut state_a = aspx::AspxChannelExtState::new();
5384 let out_default = Ac4Decoder::aspx_extend_pcm(
5385 &pcm,
5386 &tables,
5387 &cfg,
5388 None,
5389 None,
5390 None,
5391 None,
5392 None,
5393 None,
5394 None,
5395 &mut state_a,
5396 1,
5397 aspx::CompandingMode::PerSlot,
5398 None,
5399 );
5400 // Override sb0 to a different value strictly less than sbx (or
5401 // strictly between sbx and sbz) — it must change the affected
5402 // band and thus the output post-QMF synthesis.
5403 let alt_sb0 = if tables.sbx > 1 {
5404 tables.sbx - 1
5405 } else {
5406 tables.sbx + 1
5407 };
5408 let mut state_b = aspx::AspxChannelExtState::new();
5409 let out_override = Ac4Decoder::aspx_extend_pcm(
5410 &pcm,
5411 &tables,
5412 &cfg,
5413 None,
5414 None,
5415 None,
5416 None,
5417 None,
5418 None,
5419 None,
5420 &mut state_b,
5421 1,
5422 aspx::CompandingMode::PerSlot,
5423 Some(alt_sb0),
5424 );
5425 assert_eq!(out_default.len(), out_override.len());
5426 let start = 1200usize;
5427 let mut diffs = 0usize;
5428 for (a, b) in out_default[start..]
5429 .iter()
5430 .zip(out_override[start..].iter())
5431 {
5432 if (a - b).abs() > 1e-6 {
5433 diffs += 1;
5434 }
5435 }
5436 assert!(
5437 diffs > 0,
5438 "sb0 override must alter the QMF-synthesis output (diffs={diffs})"
5439 );
5440 }
5441
5442 /// Round 43: `aspx_extend_pcm` with `CompandingMode::Averaged`
5443 /// produces output that diverges from the `Off` baseline AND
5444 /// from the `PerSlot` branch — averaging collapses per-slot
5445 /// variation into a constant gain.
5446 #[test]
5447 fn aspx_extend_pcm_averaged_branch_diverges_from_per_slot() {
5448 let n_slots = 30usize;
5449 let n = n_slots * 64;
5450 let mut pcm = vec![0.0f32; n];
5451 // Mix two tones so the per-slot energy actually varies.
5452 let f1 = 600.0_f32 / 48_000.0_f32;
5453 let f2 = 1900.0_f32 / 48_000.0_f32;
5454 for (i, s) in pcm.iter_mut().enumerate() {
5455 *s = (2.0 * std::f32::consts::PI * f1 * i as f32).sin()
5456 + 0.4 * (2.0 * std::f32::consts::PI * f2 * i as f32).sin();
5457 }
5458 let cfg = aspx::AspxConfig {
5459 quant_mode_env: aspx::AspxQuantStep::Fine,
5460 start_freq: 0,
5461 stop_freq: 0,
5462 master_freq_scale: aspx::AspxMasterFreqScale::HighRes,
5463 interpolation: false,
5464 preflat: false,
5465 limiter: false,
5466 noise_sbg: 0,
5467 num_env_bits_fixfix: 0,
5468 freq_res_mode: aspx::AspxFreqResMode::Signalled,
5469 };
5470 let tables = aspx::derive_aspx_frequency_tables(&cfg, 0).unwrap();
5471 let mut state_off = aspx::AspxChannelExtState::new();
5472 let out_off = Ac4Decoder::aspx_extend_pcm(
5473 &pcm,
5474 &tables,
5475 &cfg,
5476 None,
5477 None,
5478 None,
5479 None,
5480 None,
5481 None,
5482 None,
5483 &mut state_off,
5484 1,
5485 aspx::CompandingMode::Off,
5486 None,
5487 );
5488 let mut state_per = aspx::AspxChannelExtState::new();
5489 let out_per = Ac4Decoder::aspx_extend_pcm(
5490 &pcm,
5491 &tables,
5492 &cfg,
5493 None,
5494 None,
5495 None,
5496 None,
5497 None,
5498 None,
5499 None,
5500 &mut state_per,
5501 1,
5502 aspx::CompandingMode::PerSlot,
5503 None,
5504 );
5505 let mut state_avg = aspx::AspxChannelExtState::new();
5506 let out_avg = Ac4Decoder::aspx_extend_pcm(
5507 &pcm,
5508 &tables,
5509 &cfg,
5510 None,
5511 None,
5512 None,
5513 None,
5514 None,
5515 None,
5516 None,
5517 &mut state_avg,
5518 1,
5519 aspx::CompandingMode::Averaged,
5520 None,
5521 );
5522 assert_eq!(out_off.len(), out_avg.len());
5523 assert_eq!(out_per.len(), out_avg.len());
5524 let start = 1200usize;
5525 // Averaged differs from Off (companding actually fired).
5526 let mut diffs_off_avg = 0usize;
5527 for (a, b) in out_off[start..].iter().zip(out_avg[start..].iter()) {
5528 if (a - b).abs() > 1e-6 {
5529 diffs_off_avg += 1;
5530 }
5531 }
5532 assert!(
5533 diffs_off_avg > 0,
5534 "Averaged must diverge from Off baseline (diffs={diffs_off_avg})"
5535 );
5536 // Averaged differs from PerSlot (constant scale vs per-slot scale).
5537 let mut diffs_per_avg = 0usize;
5538 for (a, b) in out_per[start..].iter().zip(out_avg[start..].iter()) {
5539 if (a - b).abs() > 1e-6 {
5540 diffs_per_avg += 1;
5541 }
5542 }
5543 assert!(
5544 diffs_per_avg > 0,
5545 "Averaged must diverge from PerSlot (diffs={diffs_per_avg})"
5546 );
5547 }
5548
5549 /// Round 44: `five_x_synced_mode` returns `Some(SyncPerSlot)` /
5550 /// `Some(SyncAveraged)` only when sync_flag=1 + the appropriate
5551 /// `compand_on[0]` / `compand_avg` flags resolve to one of the
5552 /// active sync sub-branches; returns `None` for all other states
5553 /// (no companding control, sync_flag=0, sync_flag=1+Off).
5554 #[test]
5555 fn five_x_synced_mode_resolves_each_branch() {
5556 // No companding control -> None.
5557 assert!(Ac4Decoder::five_x_synced_mode(None).is_none());
5558 // sync_flag=0 -> None (per-channel path).
5559 let cc_per = aspx::CompandingControl {
5560 sync_flag: Some(false),
5561 compand_on: vec![true, false, true, false, true],
5562 compand_avg: Some(true),
5563 };
5564 assert!(Ac4Decoder::five_x_synced_mode(Some(&cc_per)).is_none());
5565 // sync_flag=None (mono case) -> None.
5566 let cc_mono = aspx::CompandingControl {
5567 sync_flag: None,
5568 compand_on: vec![true],
5569 compand_avg: None,
5570 };
5571 assert!(Ac4Decoder::five_x_synced_mode(Some(&cc_mono)).is_none());
5572 // sync_flag=1, compand_on[0]=true -> SyncPerSlot.
5573 let cc_sync_on = aspx::CompandingControl {
5574 sync_flag: Some(true),
5575 compand_on: vec![true],
5576 compand_avg: None,
5577 };
5578 assert_eq!(
5579 Ac4Decoder::five_x_synced_mode(Some(&cc_sync_on)),
5580 Some(aspx::CompandingMode::SyncPerSlot)
5581 );
5582 // sync_flag=1, compand_on[0]=false, compand_avg=true -> SyncAveraged.
5583 let cc_sync_avg = aspx::CompandingControl {
5584 sync_flag: Some(true),
5585 compand_on: vec![false],
5586 compand_avg: Some(true),
5587 };
5588 assert_eq!(
5589 Ac4Decoder::five_x_synced_mode(Some(&cc_sync_avg)),
5590 Some(aspx::CompandingMode::SyncAveraged)
5591 );
5592 // sync_flag=1, compand_on[0]=false, compand_avg=false -> None
5593 // (companding actually off; per-channel path takes the no-op
5594 // branch).
5595 let cc_sync_off = aspx::CompandingControl {
5596 sync_flag: Some(true),
5597 compand_on: vec![false],
5598 compand_avg: Some(false),
5599 };
5600 assert!(Ac4Decoder::five_x_synced_mode(Some(&cc_sync_off)).is_none());
5601 }
5602
5603 /// Round 45: stereo-CPE M=2 synced companding helper —
5604 /// `extend_stereo_cpe_pair_with_sync_companding` writes the
5605 /// `g_synch(ts) = √(g_0(ts) · g_1(ts))` synced gain into BOTH
5606 /// channels' QMF matrices, then runs inverse QMF synthesis. This
5607 /// produces a different output than the per-channel
5608 /// `aspx_extend_pcm` path with `PerSlot` mode (which writes
5609 /// independent per-channel gains).
5610 ///
5611 /// The test pins:
5612 /// * Output cardinality + length (one extended PCM per input).
5613 /// * Output is non-silent (the HF tile copy + 0.5 flat envelope
5614 /// gain + companding apply produces audible content).
5615 /// * Synced output differs from per-channel output (proves the
5616 /// synced gain is actually applied, not silently skipped).
5617 /// * Synced output differs from `Off` output (proves the helper
5618 /// applies a non-trivial gain, not just passthrough).
5619 ///
5620 /// The numerical correctness of the geometric-mean formula is
5621 /// already exhaustively covered by
5622 /// `aspx::tests::apply_synchronised_companding_*` against the
5623 /// bare QMF helper; this test just confirms the integration glue
5624 /// (phase-1 + sync apply + phase-2) is wired correctly for the
5625 /// stereo-CPE path that drives 5_X ASPX_ACPL_3's L/R surround
5626 /// pair carriers.
5627 #[test]
5628 fn extend_stereo_cpe_pair_with_sync_companding_diverges_from_per_channel() {
5629 let cfg = aspx::AspxConfig {
5630 quant_mode_env: aspx::AspxQuantStep::Fine,
5631 start_freq: 0,
5632 stop_freq: 0,
5633 master_freq_scale: aspx::AspxMasterFreqScale::HighRes,
5634 interpolation: false,
5635 preflat: false,
5636 limiter: false,
5637 noise_sbg: 0,
5638 num_env_bits_fixfix: 0,
5639 freq_res_mode: aspx::AspxFreqResMode::Signalled,
5640 };
5641 let tables = aspx::derive_aspx_frequency_tables(&cfg, 0).unwrap();
5642 let n_slots = 24usize;
5643 let n = n_slots * 64;
5644 // Asymmetric carrier energies (8x amplitude difference) so
5645 // per-channel companding produces clearly different per-slot
5646 // gains for the two channels — synced gain (geometric mean)
5647 // is the single common scale.
5648 let mut pcm_a = vec![0.0f32; n];
5649 let mut pcm_b = vec![0.0f32; n];
5650 let f1 = 700.0_f32 / 48_000.0_f32;
5651 let f2 = 1100.0_f32 / 48_000.0_f32;
5652 for i in 0..n {
5653 pcm_a[i] = 0.04 * (2.0 * std::f32::consts::PI * f1 * i as f32).sin();
5654 pcm_b[i] = 0.32 * (2.0 * std::f32::consts::PI * f2 * i as f32).sin();
5655 }
5656 let params = CodecParameters::audio(CodecId::new("ac4"));
5657 // Synced run.
5658 let mut dec_sync = Ac4Decoder::new(¶ms);
5659 let pri_input = StereoCpeChannelInput {
5660 ch_index: 0,
5661 pcm_in: &pcm_a,
5662 framing: None,
5663 sig: None,
5664 noise: None,
5665 qmode: None,
5666 delta_dir: None,
5667 add_harmonic: None,
5668 tna_mode: None,
5669 };
5670 let sec_input = StereoCpeChannelInput {
5671 ch_index: 1,
5672 pcm_in: &pcm_b,
5673 framing: None,
5674 sig: None,
5675 noise: None,
5676 qmode: None,
5677 delta_dir: None,
5678 add_harmonic: None,
5679 tna_mode: None,
5680 };
5681 let (sync_a, sync_b) = dec_sync.extend_stereo_cpe_pair_with_sync_companding(
5682 &pri_input,
5683 &sec_input,
5684 &tables,
5685 &cfg,
5686 1,
5687 aspx::CompandingMode::SyncPerSlot,
5688 None,
5689 );
5690 // Helper-3 returns one PCM per input, both length-matched.
5691 assert_eq!(sync_a.len(), n);
5692 assert_eq!(sync_b.len(), n);
5693 // Per-channel comparison run — same inputs, but each
5694 // channel through its own `aspx_extend_pcm` with PerSlot
5695 // mode (no cross-channel synchronisation).
5696 let mut state_a = aspx::AspxChannelExtState::new();
5697 let per_a = Ac4Decoder::aspx_extend_pcm(
5698 &pcm_a,
5699 &tables,
5700 &cfg,
5701 None,
5702 None,
5703 None,
5704 None,
5705 None,
5706 None,
5707 None,
5708 &mut state_a,
5709 1,
5710 aspx::CompandingMode::PerSlot,
5711 None,
5712 );
5713 let mut state_b = aspx::AspxChannelExtState::new();
5714 let per_b = Ac4Decoder::aspx_extend_pcm(
5715 &pcm_b,
5716 &tables,
5717 &cfg,
5718 None,
5719 None,
5720 None,
5721 None,
5722 None,
5723 None,
5724 None,
5725 &mut state_b,
5726 1,
5727 aspx::CompandingMode::PerSlot,
5728 None,
5729 );
5730 // Companding-Off comparison — same shape but with the
5731 // companding gain bypassed entirely (proves the synced
5732 // helper writes a non-identity gain).
5733 let mut state_off_a = aspx::AspxChannelExtState::new();
5734 let off_a = Ac4Decoder::aspx_extend_pcm(
5735 &pcm_a,
5736 &tables,
5737 &cfg,
5738 None,
5739 None,
5740 None,
5741 None,
5742 None,
5743 None,
5744 None,
5745 &mut state_off_a,
5746 1,
5747 aspx::CompandingMode::Off,
5748 None,
5749 );
5750 let mut state_off_b = aspx::AspxChannelExtState::new();
5751 let off_b = Ac4Decoder::aspx_extend_pcm(
5752 &pcm_b,
5753 &tables,
5754 &cfg,
5755 None,
5756 None,
5757 None,
5758 None,
5759 None,
5760 None,
5761 None,
5762 &mut state_off_b,
5763 1,
5764 aspx::CompandingMode::Off,
5765 None,
5766 );
5767 let energy = |v: &[f32]| -> f64 { v.iter().map(|s| (*s as f64).powi(2)).sum() };
5768 // Outputs are non-silent.
5769 assert!(energy(&sync_a) > 0.0);
5770 assert!(energy(&sync_b) > 0.0);
5771 // Synced output differs from per-channel output —
5772 // proves the synced gain (geometric mean across both
5773 // channels) is genuinely different from the local gain
5774 // each channel would produce on its own. The geometric
5775 // mean of two unequal positive numbers is strictly
5776 // between them, so neither channel's synced output
5777 // matches its own per-channel output.
5778 let diff_a: f64 = sync_a
5779 .iter()
5780 .zip(per_a.iter())
5781 .map(|(s, p)| ((*s - *p) as f64).abs())
5782 .sum();
5783 let diff_b: f64 = sync_b
5784 .iter()
5785 .zip(per_b.iter())
5786 .map(|(s, p)| ((*s - *p) as f64).abs())
5787 .sum();
5788 assert!(
5789 diff_a > 0.0,
5790 "synced channel A must differ from per-channel A (sync gain is geometric mean of g_a, g_b which differ)"
5791 );
5792 assert!(
5793 diff_b > 0.0,
5794 "synced channel B must differ from per-channel B (sync gain is geometric mean of g_a, g_b which differ)"
5795 );
5796 // Synced output also differs from Off output (the synced
5797 // gain is non-trivial — not the identity).
5798 let diff_off_a: f64 = sync_a
5799 .iter()
5800 .zip(off_a.iter())
5801 .map(|(s, o)| ((*s - *o) as f64).abs())
5802 .sum();
5803 let diff_off_b: f64 = sync_b
5804 .iter()
5805 .zip(off_b.iter())
5806 .map(|(s, o)| ((*s - *o) as f64).abs())
5807 .sum();
5808 assert!(
5809 diff_off_a > 0.0,
5810 "synced channel A must differ from companding-Off A"
5811 );
5812 assert!(
5813 diff_off_b > 0.0,
5814 "synced channel B must differ from companding-Off B"
5815 );
5816 }
5817
5818 /// Round 44: `extend_5x_channels_with_sync_companding` returns
5819 /// one output PCM slice per input entry, in input order. The
5820 /// helper is the integration glue between the per-channel
5821 /// `aspx_extend_to_qmf` phase and the cross-channel
5822 /// `apply_synchronised_companding_across_channels` apply — this
5823 /// test pins the output cardinality + slot order. The numerical
5824 /// behaviour (geometric-mean equalisation) is exhaustively
5825 /// covered in `aspx::tests::apply_synchronised_companding_*`
5826 /// against the bare QMF helper.
5827 #[test]
5828 fn extend_5x_channels_with_sync_companding_returns_one_output_per_entry() {
5829 let cfg = aspx::AspxConfig {
5830 quant_mode_env: aspx::AspxQuantStep::Fine,
5831 start_freq: 0,
5832 stop_freq: 0,
5833 master_freq_scale: aspx::AspxMasterFreqScale::HighRes,
5834 interpolation: false,
5835 preflat: false,
5836 limiter: false,
5837 noise_sbg: 0,
5838 num_env_bits_fixfix: 0,
5839 freq_res_mode: aspx::AspxFreqResMode::Signalled,
5840 };
5841 let tables = aspx::derive_aspx_frequency_tables(&cfg, 0).unwrap();
5842 let ch = aspx::FiveXAspxChannelTrailer {
5843 framing: aspx::AspxFraming {
5844 int_class: aspx::AspxIntClass::FixFix,
5845 num_env: 1,
5846 num_noise: 1,
5847 freq_res: vec![true],
5848 var_bord_left: None,
5849 var_bord_right: None,
5850 num_rel_left: 0,
5851 num_rel_right: 0,
5852 rel_bord_left: vec![],
5853 rel_bord_right: vec![],
5854 tsg_ptr: None,
5855 },
5856 qmode_env: aspx::AspxQuantStep::Fine,
5857 data_sig: vec![],
5858 data_noise: vec![],
5859 delta_dir: aspx::AspxDeltaDir {
5860 sig_delta_dir: vec![],
5861 noise_delta_dir: vec![],
5862 },
5863 add_harmonic: None,
5864 tna_mode: None,
5865 };
5866 let trailer = aspx::FiveXAspxTrailer {
5867 xover: 0,
5868 frequency_tables: tables,
5869 primary: ch.clone(),
5870 secondary: Some(ch.clone()),
5871 };
5872 let n_slots = 24usize;
5873 let n = n_slots * 64;
5874 let mut pcm_a = vec![0.0f32; n];
5875 let mut pcm_b = vec![0.0f32; n];
5876 let f1 = 700.0_f32 / 48_000.0_f32;
5877 let f2 = 1100.0_f32 / 48_000.0_f32;
5878 for i in 0..n {
5879 pcm_a[i] = 0.05 * (2.0 * std::f32::consts::PI * f1 * i as f32).sin();
5880 pcm_b[i] = 0.8 * (2.0 * std::f32::consts::PI * f2 * i as f32).sin();
5881 }
5882 let params = CodecParameters::audio(CodecId::new("ac4"));
5883 let mut dec = Ac4Decoder::new(¶ms);
5884 let entries: Vec<SyncCompandingChannelEntry<'_>> = vec![
5885 (0, &pcm_a, &trailer, &trailer.primary, &cfg, None),
5886 (
5887 3,
5888 &pcm_b,
5889 &trailer,
5890 trailer.secondary.as_ref().unwrap(),
5891 &cfg,
5892 None,
5893 ),
5894 ];
5895 let out = dec.extend_5x_channels_with_sync_companding(
5896 &entries,
5897 1,
5898 aspx::CompandingMode::SyncPerSlot,
5899 );
5900 assert_eq!(out.len(), 2);
5901 // Slot indices preserved in input order.
5902 assert_eq!(out[0].0, 0);
5903 assert_eq!(out[1].0, 3);
5904 // Each PCM matches the input length.
5905 assert_eq!(out[0].1.len(), n);
5906 assert_eq!(out[1].1.len(), n);
5907 }
5908}