Skip to main content

oxideav_ac4/
acpl_synth.rs

1//! A-CPL (Advanced Coupling) QMF synthesis math — ETSI TS 103 190-1
2//! §5.7.7.
3#![allow(clippy::excessive_precision, clippy::needless_range_loop)]
4//!
5//! What this module covers today:
6//!
7//! * **§5.7.7.7 Differential decoding + dequantization** —
8//!   [`differential_decode`] (Pseudocode 121) absorbs the per-band
9//!   Huffman deltas produced by `acpl_data_*ch()` and re-creates the
10//!   absolute quantised parameter array `acpl_<SET>_q[ps][pb]` for both
11//!   `DIFF_FREQ` and `DIFF_TIME` directions, chaining the
12//!   `acpl_SET_q_prev` vector across parameter sets and AC-4 frames.
13//!   The four index→float dequantisation tables ([`ALPHA_DQ_FINE`],
14//!   [`ALPHA_DQ_COARSE`], [`BETA_DQ_FINE`], [`BETA_DQ_COARSE`] +
15//!   [`IBETA_FINE`] / [`IBETA_COARSE`], Tables 203-206) lift the
16//!   integer indices to real-valued α / β coefficients;
17//!   [`dequantize_beta3`] / [`dequantize_gamma`] handle the simpler
18//!   delta-multiplier paths from Tables 207 / 208. ALPHA / BETA / BETA3
19//!   are coupled — the alpha index selects an `ibeta` row in Table 204
20//!   / 206 that the corresponding beta lookup uses, so
21//!   [`dequantize_alpha_beta`] returns both arrays in one shot.
22//!
23//! * **§5.7.7.3 Time interpolation** — [`interpolate`] (Pseudocode 109)
24//!   linearly interpolates per-QMF-subsample (sb, ts) values from the
25//!   per-parameter-set vectors using the `sb_to_pb` table-197 mapping,
26//!   carrying the previous frame's `acpl_param_prev[sb]` array. Both
27//!   smooth and steep interpolation modes are wired (smooth = linear
28//!   between borders, steep = piecewise-constant at
29//!   `acpl_param_timeslot[]`).
30//!
31//! * **§5.7.7.4.2 Decorrelator** — [`InputSignalModifier`] implements
32//!   Pseudocode 111: a frequency-banded all-pass IIR (3 regions × 3
33//!   coefficient sets per Tables 199 / 200 / 201) preceded by a
34//!   per-region constant delay (Table 198: `7 / 10 / 12` slots for
35//!   `k0 / k1 / k2`). Three modules `D0`, `D1`, `D2` are exposed via
36//!   [`Decorrelator`] for the `u0 / u1 / u2` paths.
37//!
38//! * **§5.7.7.4.3 Transient ducker** — [`TransientDucker`]
39//!   (Pseudocodes 112-114) computes the per-parameter-band peak-decay /
40//!   smooth / smooth-peak-diff rolling state and applies the resulting
41//!   `duck_gain[pb]` to the decorrelated signal via the `sb_to_pb`
42//!   mapping.
43//!
44//! * **§5.7.7.5 Channel pair element** — [`acpl_module`]
45//!   (Pseudocode 116) is the canonical 2-channel synthesis: it runs
46//!   the M/S split below `acpl_qmf_band` and the mixed
47//!   `0.5 * (x0*(1±α) ± y*β)` path above it. [`AcplCpeState`] bundles
48//!   the previous-frame `acpl_alpha_prev` / `acpl_beta_prev` arrays
49//!   plus the decorrelator + ducker state for the full
50//!   `Pseudocode 115` channel-pair synthesis path.
51//!
52//! * **§5.7.7.6.1 Multichannel `ASPX_ACPL_1` / `ASPX_ACPL_2`** —
53//!   [`run_pseudocode_117_5x`] (Pseudocode 117) wraps two parallel
54//!   [`run_pseudocode_115_pair`] passes (D0 for the L-side ACplModule,
55//!   D1 for the R-side) and forms the five 5.X output channels from
56//!   the L/R/C carriers (plus optional Ls/Rs carriers for ASPX_ACPL_1).
57//!
58//! * **§5.7.7.6.2 Multichannel `ASPX_ACPL_3`** — [`transform`]
59//!   (Pseudocode 119), [`acpl_module2`] / [`acpl_module3`] (also
60//!   Pseudocode 119) and [`run_pseudocode_118_5x`] (Pseudocode 118)
61//!   synthesise the five output channels (`z0..z4`) from the two A-CPL
62//!   carrier channels (`x0`, `x1`), the centre passthrough `x2`, and the
63//!   six gamma matrices `g1..g6`. The three parallel decorrelator paths
64//!   (D0/D1/D2) are driven by `Transform()`-mixed inputs and routed
65//!   through `ACplModule2` (gamma+alpha+beta combiner) followed by
66//!   `ACplModule3` (beta3 cross-residual term) per the spec.
67//!
68//! What is NOT covered yet (TS 103 190-1):
69//!
70//! * Hooking the multichannel synthesis into the frame-level decoder
71//!   pipeline (the asf walker still parses `acpl_data_*` but the 5_X
72//!   walker doesn't yet drive [`run_pseudocode_118_5x`] /
73//!   [`run_pseudocode_117_5x`]).
74
75use crate::acpl::{sb_to_pb, AcplHuffParam, AcplQuantMode};
76use crate::qmf::NUM_QMF_SUBBANDS;
77
78// =====================================================================
79// §5.7.7.7 Tables 203 / 204 / 205 / 206 — ALPHA / BETA dequantisation
80// =====================================================================
81
82/// `alpha_dq[alpha_q]` for fine quantisation per Table 203.
83/// Length 33 (`alpha_q ∈ 0..=32`).
84#[rustfmt::skip]
85pub const ALPHA_DQ_FINE: [f32; 33] = [
86    -2.000000, -1.809375, -1.637500, -1.484375, -1.350000, -1.234375,
87    -1.137500, -1.059375, -1.000000, -0.940625, -0.862500, -0.765625,
88    -0.650000, -0.515625, -0.362500, -0.190625,  0.000000,  0.190625,
89     0.362500,  0.515625,  0.650000,  0.765625,  0.862500,  0.940625,
90     1.000000,  1.059375,  1.137500,  1.234375,  1.350000,  1.484375,
91     1.637500,  1.809375,  2.000000,
92];
93
94/// `ibeta[alpha_q]` for fine quantisation per Table 203 (column 2).
95/// Used to pick a column out of `BETA_DQ_FINE` for the matching beta
96/// dequant.
97#[rustfmt::skip]
98pub const IBETA_FINE: [u8; 33] = [
99    0, 1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5,
100    6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 0,
101];
102
103/// `alpha_dq[alpha_q]` for coarse quantisation per Table 205. Length 17.
104#[rustfmt::skip]
105pub const ALPHA_DQ_COARSE: [f32; 17] = [
106    -2.000000, -1.637500, -1.350000, -1.137500, -1.000000, -0.862500,
107    -0.650000, -0.362500,  0.000000,  0.362500,  0.650000,  0.862500,
108     1.000000,  1.137500,  1.350000,  1.637500,  2.000000,
109];
110
111/// `ibeta[alpha_q]` for coarse quantisation per Table 205 (column 2).
112#[rustfmt::skip]
113pub const IBETA_COARSE: [u8; 17] = [
114    0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 0,
115];
116
117/// `beta_dq[beta_q][ibeta]` for fine quantisation per Table 204.
118/// Indexed by `[beta_q][ibeta]` with `beta_q ∈ 0..=8`, `ibeta ∈ 0..=8`.
119/// The spec table is signed via the `beta_q` ↔ huffman delta convention
120/// (positive index = positive coefficient); here the table only carries
121/// the magnitude — the caller flips the sign when `beta_q < 0`.
122#[rustfmt::skip]
123pub const BETA_DQ_FINE: [[f32; 9]; 9] = [
124    // beta_q = 0
125    [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000],
126    // beta_q = 1
127    [0.2375000, 0.2035449, 0.1729297, 0.1456543, 0.1217188, 0.1011230, 0.0838672, 0.0699512, 0.0593750],
128    // beta_q = 2
129    [0.5500000, 0.4713672, 0.4004688, 0.3373047, 0.2818750, 0.2341797, 0.1942188, 0.1619922, 0.1375000],
130    // beta_q = 3
131    [0.9375000, 0.8034668, 0.6826172, 0.5749512, 0.4804688, 0.3991699, 0.3310547, 0.2761230, 0.2343750],
132    // beta_q = 4
133    [1.4000000, 1.1998440, 1.0193750, 0.8585938, 0.7175000, 0.5960938, 0.4943750, 0.4123438, 0.3500000],
134    // beta_q = 5
135    [1.9375000, 1.6604980, 1.4107420, 1.1882319, 0.9929688, 0.8249512, 0.6841797, 0.5706543, 0.4843750],
136    // beta_q = 6
137    [2.5500000, 2.1854300, 1.8567190, 1.5638670, 1.3068750, 1.0857420, 0.9004688, 0.7510547, 0.6375000],
138    // beta_q = 7
139    [3.2375000, 2.7746389, 2.3573050, 1.9854980, 1.6592190, 1.3784670, 1.1432420, 0.9535449, 0.8093750],
140    // beta_q = 8
141    [4.0000000, 3.4281249, 2.9124999, 2.4531250, 2.0500000, 1.7031250, 1.4125000, 1.1781250, 1.0000000],
142];
143
144/// `beta_dq[beta_q][ibeta]` for coarse quantisation per Table 206.
145/// Indexed by `[beta_q][ibeta]` with `beta_q ∈ 0..=4`, `ibeta ∈ 0..=4`.
146#[rustfmt::skip]
147pub const BETA_DQ_COARSE: [[f32; 5]; 5] = [
148    // beta_q = 0
149    [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000],
150    // beta_q = 1
151    [0.5500000, 0.4004688, 0.2818750, 0.1942188, 0.1375000],
152    // beta_q = 2
153    [1.4000000, 1.0193750, 0.7175000, 0.4943750, 0.3500000],
154    // beta_q = 3
155    [2.5500000, 1.8567190, 1.3068750, 0.9004688, 0.6375000],
156    // beta_q = 4
157    [4.0000000, 2.9124999, 2.0500000, 1.4125000, 1.0000000],
158];
159
160/// `beta3` dequantisation delta per Table 207. Multiply the recovered
161/// `beta3_q` value by this delta to get `acpl_beta_3_dq`.
162pub fn beta3_delta(qm: AcplQuantMode) -> f32 {
163    match qm {
164        AcplQuantMode::Fine => 0.125,
165        AcplQuantMode::Coarse => 0.25,
166    }
167}
168
169/// `gamma` dequantisation delta per Table 208 (`g1..g6`). Both modes
170/// expressed as the spec's exact `n / 16384` ratios.
171pub fn gamma_delta(qm: AcplQuantMode) -> f32 {
172    match qm {
173        AcplQuantMode::Fine => 1638.0 / 16384.0,
174        AcplQuantMode::Coarse => 3276.0 / 16384.0,
175    }
176}
177
178/// Look up `(alpha_dq, ibeta)` from a recovered alpha quantised index
179/// per Table 203 (Fine) or Table 205 (Coarse).
180///
181/// The huffman pipeline already produces a *signed* `alpha_q` in the
182/// range `-N/2 ..= +N/2` (where `N = ALPHA_DQ_*.len() - 1`) thanks to
183/// the F0/DF/DT codebooks' `cb_off`. The dequant tables are addressed
184/// by the *unsigned* lane index `alpha_q + cb_off`, where `cb_off =
185/// N/2`. We re-add it here.
186pub fn dequantize_alpha_index(qm: AcplQuantMode, alpha_q: i32) -> (f32, u8) {
187    match qm {
188        AcplQuantMode::Fine => {
189            let lane = (alpha_q + 16).clamp(0, 32) as usize;
190            (ALPHA_DQ_FINE[lane], IBETA_FINE[lane])
191        }
192        AcplQuantMode::Coarse => {
193            let lane = (alpha_q + 8).clamp(0, 16) as usize;
194            (ALPHA_DQ_COARSE[lane], IBETA_COARSE[lane])
195        }
196    }
197}
198
199/// Look up the dequantised beta from `beta_q` and the matching
200/// `ibeta` produced by [`dequantize_alpha_index`].
201///
202/// Per Table 204 / 206, beta values are unsigned magnitudes — the
203/// recovered `beta_q` carries the sign, so we flip the lookup once at
204/// the end.
205pub fn dequantize_beta_index(qm: AcplQuantMode, beta_q: i32, ibeta: u8) -> f32 {
206    let mag = beta_q.unsigned_abs() as usize;
207    let val = match qm {
208        AcplQuantMode::Fine => {
209            let row = mag.min(8);
210            let col = (ibeta as usize).min(8);
211            BETA_DQ_FINE[row][col]
212        }
213        AcplQuantMode::Coarse => {
214            let row = mag.min(4);
215            let col = (ibeta as usize).min(4);
216            BETA_DQ_COARSE[row][col]
217        }
218    };
219    if beta_q < 0 {
220        -val
221    } else {
222        val
223    }
224}
225
226// =====================================================================
227// §5.7.7.7 Pseudocode 121 — differential decoding
228// =====================================================================
229
230/// Differential decoding state per parameter-set type
231/// (`acpl_<SET>_q_prev`).
232///
233/// The spec mandates an initial all-zero `q_prev` for the first AC-4
234/// frame, and carries the last `q[ps]` row forward across frames to
235/// seed the next `DIFF_TIME` decode.
236#[derive(Debug, Clone)]
237pub struct AcplDiffState {
238    /// Carries `acpl_<SET>_q[num_pset-1]` across calls. Length =
239    /// `acpl_num_param_bands` once primed; the first call sees an
240    /// empty vector and fills it.
241    pub q_prev: Vec<i32>,
242}
243
244impl AcplDiffState {
245    pub fn new() -> Self {
246        Self { q_prev: Vec::new() }
247    }
248
249    /// Replace the running `q_prev` row.
250    pub fn carry(&mut self, row: &[i32]) {
251        self.q_prev.clear();
252        self.q_prev.extend_from_slice(row);
253    }
254}
255
256impl Default for AcplDiffState {
257    fn default() -> Self {
258        Self::new()
259    }
260}
261
262/// Apply `Pseudocode 121` differential decoding to the per-parameter-set
263/// huffman-decoded deltas to recover the absolute `acpl_<SET>_q` array.
264///
265/// `params[ps]` is one parameter set's worth of huffman output (with
266/// either `DIFF_FREQ` or `DIFF_TIME` direction); `num_bands` is
267/// `acpl_num_param_bands`. The returned matrix is `num_pset`
268/// rows × `num_bands` columns. The state `q_prev` is updated to the
269/// last decoded row for forward chaining.
270pub fn differential_decode(
271    params: &[AcplHuffParam],
272    num_bands: u32,
273    state: &mut AcplDiffState,
274) -> Vec<Vec<i32>> {
275    let nb = num_bands as usize;
276    if state.q_prev.len() != nb {
277        state.q_prev = vec![0; nb];
278    }
279    let mut out = Vec::with_capacity(params.len());
280    for p in params {
281        // Per the spec the huffman delta vector should be exactly
282        // `num_bands` long (per `acpl_data_*` framing); be defensive
283        // for unusual lengths by taking what we can and zero-padding.
284        let mut row = vec![0i32; nb];
285        if !p.direction_time {
286            // DIFF_FREQ — first value is absolute, rest accumulate.
287            if !p.values.is_empty() {
288                row[0] = p.values[0];
289                for i in 1..nb {
290                    let d = p.values.get(i).copied().unwrap_or(0);
291                    row[i] = row[i - 1] + d;
292                }
293            }
294        } else {
295            // DIFF_TIME — each band reuses q_prev[i] + delta.
296            for i in 0..nb {
297                let d = p.values.get(i).copied().unwrap_or(0);
298                row[i] = state.q_prev[i] + d;
299            }
300        }
301        state.q_prev = row.clone();
302        out.push(row);
303    }
304    out
305}
306
307// =====================================================================
308// §5.7.7.7 Helpers — turn `acpl_<SET>_q` into dequantised f32 arrays
309// =====================================================================
310
311/// Dequantise one (or two) parameter sets' worth of (alpha, beta)
312/// indices into their floating-point counterparts. The returned matrix
313/// shape mirrors the input: `pset × num_bands`.
314///
315/// The shared `ibeta` linkage between Tables 203/204 (Fine) and
316/// Tables 205/206 (Coarse) is honoured per the spec — beta is looked up
317/// with the alpha column's `ibeta`.
318pub fn dequantize_alpha_beta(
319    alpha_q: &[Vec<i32>],
320    beta_q: &[Vec<i32>],
321    qm: AcplQuantMode,
322) -> (Vec<Vec<f32>>, Vec<Vec<f32>>) {
323    debug_assert_eq!(alpha_q.len(), beta_q.len());
324    let mut alpha_dq = Vec::with_capacity(alpha_q.len());
325    let mut beta_dq = Vec::with_capacity(beta_q.len());
326    for ps in 0..alpha_q.len() {
327        let n = alpha_q[ps].len();
328        let mut a_row = vec![0.0f32; n];
329        let mut b_row = vec![0.0f32; n];
330        for i in 0..n {
331            let (a_val, ibeta) = dequantize_alpha_index(qm, alpha_q[ps][i]);
332            a_row[i] = a_val;
333            let bq = beta_q[ps].get(i).copied().unwrap_or(0);
334            b_row[i] = dequantize_beta_index(qm, bq, ibeta);
335        }
336        alpha_dq.push(a_row);
337        beta_dq.push(b_row);
338    }
339    (alpha_dq, beta_dq)
340}
341
342/// Dequantise `beta3_q` to `acpl_beta_3_dq` per Table 207.
343pub fn dequantize_beta3(beta3_q: &[Vec<i32>], qm: AcplQuantMode) -> Vec<Vec<f32>> {
344    let delta = beta3_delta(qm);
345    beta3_q
346        .iter()
347        .map(|row| row.iter().map(|&q| (q as f32) * delta).collect())
348        .collect()
349}
350
351/// Dequantise `gamma_q` to `g_dq` per Table 208.
352pub fn dequantize_gamma(gamma_q: &[Vec<i32>], qm: AcplQuantMode) -> Vec<Vec<f32>> {
353    let delta = gamma_delta(qm);
354    gamma_q
355        .iter()
356        .map(|row| row.iter().map(|&q| (q as f32) * delta).collect())
357        .collect()
358}
359
360// =====================================================================
361// §5.7.7.3 Pseudocode 109 — interpolate()
362// =====================================================================
363
364/// One frame's worth of dequantised parameter values across (pset, sb).
365///
366/// Indexed `[ps][sb]` after the spec's `interpolate()` rewrite (which
367/// folds the `sb_to_pb()` lookup into the storage). The previous
368/// frame's last-row values are carried in `prev` (length
369/// `num_qmf_subbands`).
370#[derive(Debug, Clone)]
371pub struct InterpInputs<'a> {
372    /// `acpl_param_dq[ps][sb]` — already expanded over `sb_to_pb()`.
373    /// Outer length = `num_pset`, inner = `num_qmf_subbands`.
374    pub by_pset: &'a [Vec<f32>],
375    /// `acpl_param_prev[sb]` from the previous frame's last set.
376    pub prev: &'a [f32],
377}
378
379/// `interpolate(acpl_param, num_pset, sb, ts)` per §5.7.7.3
380/// Pseudocode 109.
381///
382/// `num_ts` is `num_qmf_timeslots`. `interpolation_type == 0` means
383/// smooth, `== 1` means steep (driven by `acpl_param_timeslot[]` —
384/// `param_timeslots` here).
385#[allow(clippy::too_many_arguments)]
386pub fn interpolate(
387    inputs: &InterpInputs<'_>,
388    num_pset: u32,
389    sb: u32,
390    ts: u32,
391    num_ts: u32,
392    interp_steep: bool,
393    param_timeslots: &[u8],
394) -> f32 {
395    let sb_idx = sb as usize;
396    if !interp_steep {
397        // Smooth interpolation
398        if num_pset == 1 {
399            let p = inputs.by_pset[0].get(sb_idx).copied().unwrap_or(0.0);
400            let prev = inputs.prev.get(sb_idx).copied().unwrap_or(0.0);
401            let delta = p - prev;
402            prev + ((ts + 1) as f32) * delta / (num_ts as f32)
403        } else {
404            // 2 parameter sets
405            let ts_2 = num_ts / 2;
406            let p0 = inputs.by_pset[0].get(sb_idx).copied().unwrap_or(0.0);
407            let p1 = inputs.by_pset[1].get(sb_idx).copied().unwrap_or(0.0);
408            if ts < ts_2 {
409                let prev = inputs.prev.get(sb_idx).copied().unwrap_or(0.0);
410                let delta = p0 - prev;
411                prev + ((ts + 1) as f32) * delta / (ts_2 as f32)
412            } else {
413                let delta = p1 - p0;
414                p0 + ((ts - ts_2 + 1) as f32) * delta / ((num_ts - ts_2) as f32)
415            }
416        }
417    } else {
418        // Steep interpolation
419        if num_pset == 1 {
420            let bound = param_timeslots.first().copied().unwrap_or(0) as u32;
421            if ts < bound {
422                inputs.prev.get(sb_idx).copied().unwrap_or(0.0)
423            } else {
424                inputs.by_pset[0].get(sb_idx).copied().unwrap_or(0.0)
425            }
426        } else {
427            let bound0 = param_timeslots.first().copied().unwrap_or(0) as u32;
428            let bound1 = param_timeslots.get(1).copied().unwrap_or(0) as u32;
429            if ts < bound0 {
430                inputs.prev.get(sb_idx).copied().unwrap_or(0.0)
431            } else if ts < bound1 {
432                inputs.by_pset[0].get(sb_idx).copied().unwrap_or(0.0)
433            } else {
434                inputs.by_pset[1].get(sb_idx).copied().unwrap_or(0.0)
435            }
436        }
437    }
438}
439
440/// Expand a per-parameter-band array `pb_vals[pset][pb]` to a per-QMF-
441/// subband array `sb_vals[pset][sb]` via `sb_to_pb()` (§5.7.7.2 Table
442/// 197). This is the standard input to [`interpolate`]'s `by_pset` field.
443pub fn expand_pb_to_sb(pb_vals: &[Vec<f32>], num_param_bands: u32) -> Vec<Vec<f32>> {
444    pb_vals
445        .iter()
446        .map(|row| {
447            let mut out = vec![0.0f32; NUM_QMF_SUBBANDS];
448            for sb in 0..NUM_QMF_SUBBANDS {
449                let pb = sb_to_pb(sb as u32, num_param_bands) as usize;
450                out[sb] = row.get(pb).copied().unwrap_or(0.0);
451            }
452            out
453        })
454        .collect()
455}
456
457/// Update the `prev[sb]` array per §5.7.7.3 Pseudocode 110, using the
458/// last parameter set in the current frame.
459pub fn update_param_prev(prev: &mut Vec<f32>, last_set_sb: &[f32]) {
460    prev.clear();
461    prev.extend_from_slice(last_set_sb);
462    // Pad / truncate to NUM_QMF_SUBBANDS so the next frame sees a
463    // canonically-shaped state.
464    prev.resize(NUM_QMF_SUBBANDS, 0.0);
465}
466
467// =====================================================================
468// §5.7.7.4.2 Decorrelator — Tables 198 / 199 / 200 / 201 + Pseudocode
469// 111
470// =====================================================================
471
472/// Per-subband region delay (Table 198).
473pub fn region_delay(sb: u32) -> usize {
474    match sb {
475        0..=6 => 7,
476        7..=22 => 10,
477        _ => 12,
478    }
479}
480
481/// Per-subband filter length (Table 198).
482pub fn region_filter_length(sb: u32) -> usize {
483    match sb {
484        0..=6 => 7,
485        7..=22 => 4,
486        _ => 2,
487    }
488}
489
490/// Decorrelator selector for the three parallel modules.
491#[derive(Debug, Clone, Copy)]
492pub enum DecorrelatorId {
493    D0,
494    D1,
495    D2,
496}
497
498/// Region k0 coefficients (Table 199), indexed `[D][i]` with i in
499/// `0..=7`. `D0` = column 0, `D1` = column 1, `D2` = column 2.
500#[rustfmt::skip]
501pub const A_K0: [[f64; 8]; 3] = [
502    // D0
503    [1.0000, 0.5306, -0.4533, -0.6248,  0.0424,  0.4237,  0.4311,  0.1688],
504    // D1
505    [1.0000, -0.4178, 0.1082, -0.2368, -0.1014, -0.1052, -0.3528,  0.4665],
506    // D2
507    [1.0000, 0.4007,  0.4747,  0.2611, -0.1211, -0.4248, -0.2989, -0.1932],
508];
509
510/// Region k1 coefficients (Table 200), indexed `[D][i]` with i in
511/// `0..=4`.
512#[rustfmt::skip]
513pub const A_K1: [[f64; 5]; 3] = [
514    [1.0000,  0.5561, -0.3039, -0.5024, -0.1850],
515    [1.0000,  0.0425,  0.3235, -0.1556,  0.4958],
516    [1.0000, -0.4361,  0.0345,  0.5215, -0.4178],
517];
518
519/// Region k2 coefficients (Table 201), indexed `[D][i]` with i in
520/// `0..=2`.
521#[rustfmt::skip]
522pub const A_K2: [[f64; 3]; 3] = [
523    [1.0000,  0.5773,  0.3321],
524    [1.0000,  0.2327, -0.3901],
525    [1.0000, -0.6057,  0.3804],
526];
527
528/// Look up the `a[i]` coefficient vector for the requested decorrelator
529/// `D` and subband region (k0 / k1 / k2). Returned slice length matches
530/// `region_filter_length(sb) + 1`.
531pub fn region_coeffs(d: DecorrelatorId, sb: u32) -> &'static [f64] {
532    let col = match d {
533        DecorrelatorId::D0 => 0,
534        DecorrelatorId::D1 => 1,
535        DecorrelatorId::D2 => 2,
536    };
537    match sb {
538        0..=6 => &A_K0[col][..],
539        7..=22 => &A_K1[col][..],
540        _ => &A_K2[col][..],
541    }
542}
543
544/// Per-channel + per-decorrelator running state for
545/// [`InputSignalModifier`]. Carries the tail of `x[ts-i-delay][sb]`
546/// (input history) and `y[ts-i][sb]` (output history) needed by
547/// Pseudocode 111. Sized to the maximum delay+filterLength = 12 + 2 =
548/// 14 in the worst region; we allocate 24 per subband for slack.
549#[derive(Debug, Clone)]
550pub struct InputSignalModifier {
551    /// Decorrelator id (D0/D1/D2).
552    pub which: DecorrelatorId,
553    /// `x_hist[sb][k]` = previous input sample at offset `k` slots in
554    /// the past for QMF subband `sb`. Complex (re, im) pair.
555    pub x_hist: Vec<Vec<(f32, f32)>>,
556    /// `y_hist[sb][k]` = previous output sample at offset `k` slots in
557    /// the past.
558    pub y_hist: Vec<Vec<(f32, f32)>>,
559}
560
561impl InputSignalModifier {
562    pub fn new(which: DecorrelatorId) -> Self {
563        let depth = 24usize;
564        Self {
565            which,
566            x_hist: vec![vec![(0.0, 0.0); depth]; NUM_QMF_SUBBANDS],
567            y_hist: vec![vec![(0.0, 0.0); depth]; NUM_QMF_SUBBANDS],
568        }
569    }
570
571    /// Reset the running history (e.g. on substream restart).
572    pub fn reset(&mut self) {
573        for h in &mut self.x_hist {
574            for v in h {
575                *v = (0.0, 0.0);
576            }
577        }
578        for h in &mut self.y_hist {
579            for v in h {
580                *v = (0.0, 0.0);
581            }
582        }
583    }
584
585    /// Process one (sb, ts) sample of the input matrix `x`. Returns the
586    /// decorrelated output `y[ts][sb]`. The caller pushes the input
587    /// (re, im) and receives the corresponding output.
588    pub fn process_sample(&mut self, sb: u32, x_in: (f32, f32)) -> (f32, f32) {
589        let sb_idx = sb as usize;
590        let delay = region_delay(sb);
591        let filter_length = region_filter_length(sb);
592        let a = region_coeffs(self.which, sb);
593        // Push x_in into x_hist (offset 0 = brand-new sample).
594        let xh = &mut self.x_hist[sb_idx];
595        xh.rotate_right(1);
596        xh[0] = x_in;
597        let yh = &mut self.y_hist[sb_idx];
598
599        // b[0] = a[filterLength], y = b[0] * x[ts-delay] / a[0]
600        let b0 = a[filter_length];
601        let a0 = a[0];
602        let xd = if delay < xh.len() {
603            xh[delay]
604        } else {
605            (0.0, 0.0)
606        };
607        let mut y_re = (b0 / a0) * xd.0 as f64;
608        let mut y_im = (b0 / a0) * xd.1 as f64;
609        for i in 1..=filter_length {
610            let bi = a[filter_length - i];
611            let xi_idx = i + delay;
612            let xi = if xi_idx < xh.len() {
613                xh[xi_idx]
614            } else {
615                (0.0, 0.0)
616            };
617            // y_hist[i-1] is the output at ts-i (since after rotate_right
618            // we'll push current y at offset 0).
619            let yi = if i - 1 < yh.len() {
620                yh[i - 1]
621            } else {
622                (0.0, 0.0)
623            };
624            let ai = a[i];
625            y_re += (bi * xi.0 as f64 - ai * yi.0 as f64) / a0;
626            y_im += (bi * xi.1 as f64 - ai * yi.1 as f64) / a0;
627        }
628        let y_out = (y_re as f32, y_im as f32);
629        // Push y_out into y_hist (rotate_right shifts older outputs up).
630        yh.rotate_right(1);
631        yh[0] = y_out;
632        y_out
633    }
634}
635
636/// Triplet of decorrelator modules for the three `u_x` paths used by
637/// the multichannel A-CPL configurations.
638#[derive(Debug, Clone)]
639pub struct Decorrelator {
640    pub d0: InputSignalModifier,
641    pub d1: InputSignalModifier,
642    pub d2: InputSignalModifier,
643}
644
645impl Decorrelator {
646    pub fn new() -> Self {
647        Self {
648            d0: InputSignalModifier::new(DecorrelatorId::D0),
649            d1: InputSignalModifier::new(DecorrelatorId::D1),
650            d2: InputSignalModifier::new(DecorrelatorId::D2),
651        }
652    }
653}
654
655impl Default for Decorrelator {
656    fn default() -> Self {
657        Self::new()
658    }
659}
660
661// =====================================================================
662// §5.7.7.4.3 Transient ducker — Pseudocodes 112 / 113 / 114
663// =====================================================================
664
665/// Spec constants from Pseudocode 112.
666pub const DUCKER_ALPHA: f32 = 0.76592833836465;
667pub const DUCKER_ALPHA_SMOOTH: f32 = 0.25;
668pub const DUCKER_GAMMA: f32 = 1.5;
669pub const DUCKER_EPSILON: f32 = 1.0e-9;
670
671/// `acpl_max_num_param_bands = 15` per Pseudocode 112.
672pub const ACPL_MAX_NUM_PARAM_BANDS: usize = 15;
673
674/// Persistent transient-ducker state across AC-4 frames.
675#[derive(Debug, Clone)]
676pub struct TransientDucker {
677    pub p_peak_decay_prev: [f32; ACPL_MAX_NUM_PARAM_BANDS],
678    pub p_smooth_prev: [f32; ACPL_MAX_NUM_PARAM_BANDS],
679    pub smooth_peak_diff_prev: [f32; ACPL_MAX_NUM_PARAM_BANDS],
680}
681
682impl TransientDucker {
683    pub fn new() -> Self {
684        Self {
685            p_peak_decay_prev: [0.0; ACPL_MAX_NUM_PARAM_BANDS],
686            p_smooth_prev: [0.0; ACPL_MAX_NUM_PARAM_BANDS],
687            smooth_peak_diff_prev: [0.0; ACPL_MAX_NUM_PARAM_BANDS],
688        }
689    }
690
691    /// Reset state (called when `acpl_param_prev` is reinitialised).
692    pub fn reset(&mut self) {
693        *self = Self::new();
694    }
695
696    /// Compute `duck_gain[pb]` from the per-pb energy array
697    /// (Pseudocode 112). Updates the carrying state.
698    pub fn update(
699        &mut self,
700        p_energy: &[f32; ACPL_MAX_NUM_PARAM_BANDS],
701    ) -> [f32; ACPL_MAX_NUM_PARAM_BANDS] {
702        let mut duck = [1.0f32; ACPL_MAX_NUM_PARAM_BANDS];
703        for pb in 0..ACPL_MAX_NUM_PARAM_BANDS {
704            let p_peak_decay = if DUCKER_ALPHA * self.p_peak_decay_prev[pb] < p_energy[pb] {
705                p_energy[pb]
706            } else {
707                DUCKER_ALPHA * self.p_peak_decay_prev[pb]
708            };
709            let smooth = (1.0 - DUCKER_ALPHA_SMOOTH) * self.p_smooth_prev[pb]
710                + DUCKER_ALPHA_SMOOTH * p_energy[pb];
711            let smooth_peak_diff = (1.0 - DUCKER_ALPHA_SMOOTH) * self.smooth_peak_diff_prev[pb]
712                + DUCKER_ALPHA_SMOOTH * (p_peak_decay - p_energy[pb]);
713            let g = if DUCKER_GAMMA * smooth_peak_diff > smooth {
714                smooth / (DUCKER_GAMMA * (smooth_peak_diff + DUCKER_EPSILON))
715            } else {
716                1.0
717            };
718            duck[pb] = g;
719            self.p_peak_decay_prev[pb] = p_peak_decay;
720            self.p_smooth_prev[pb] = smooth;
721            self.smooth_peak_diff_prev[pb] = smooth_peak_diff;
722        }
723        duck
724    }
725}
726
727impl Default for TransientDucker {
728    fn default() -> Self {
729        Self::new()
730    }
731}
732
733/// Compute `p_energy[pb]` from one column of QMF subbands per
734/// Pseudocode 113 (`pb` = parameter band, summed |x|² over the QMF
735/// subbands mapping to that pb via Table 197).
736pub fn compute_p_energy(
737    x: &[(f32, f32); NUM_QMF_SUBBANDS],
738    num_param_bands: u32,
739) -> [f32; ACPL_MAX_NUM_PARAM_BANDS] {
740    let mut e = [0.0f32; ACPL_MAX_NUM_PARAM_BANDS];
741    for sb in 0..NUM_QMF_SUBBANDS as u32 {
742        let pb = sb_to_pb(sb, num_param_bands) as usize;
743        let (re, im) = x[sb as usize];
744        e[pb] += re * re + im * im;
745    }
746    e
747}
748
749/// Apply the per-pb `duck_gain` to a single QMF column (Pseudocode 114).
750pub fn apply_transient_ducker(
751    x: &[(f32, f32); NUM_QMF_SUBBANDS],
752    duck_gain: &[f32; ACPL_MAX_NUM_PARAM_BANDS],
753    num_param_bands: u32,
754) -> [(f32, f32); NUM_QMF_SUBBANDS] {
755    let mut out = *x;
756    for sb in 0..NUM_QMF_SUBBANDS as u32 {
757        let pb = sb_to_pb(sb, num_param_bands) as usize;
758        let g = duck_gain[pb];
759        out[sb as usize].0 *= g;
760        out[sb as usize].1 *= g;
761    }
762    out
763}
764
765// =====================================================================
766// §5.7.7.5 Pseudocode 115 / 116 — channel-pair element synthesis
767// =====================================================================
768
769/// Persistent state for a single ACplModule (carries the previous
770/// frame's `acpl_alpha_prev[sb]` / `acpl_beta_prev[sb]` and the
771/// decorrelator + ducker scratch).
772#[derive(Debug, Clone)]
773pub struct AcplCpeState {
774    pub alpha_prev_sb: Vec<f32>,
775    pub beta_prev_sb: Vec<f32>,
776    pub decorrelator: InputSignalModifier,
777    pub ducker: TransientDucker,
778}
779
780impl AcplCpeState {
781    pub fn new(which: DecorrelatorId) -> Self {
782        Self {
783            alpha_prev_sb: vec![0.0; NUM_QMF_SUBBANDS],
784            beta_prev_sb: vec![0.0; NUM_QMF_SUBBANDS],
785            decorrelator: InputSignalModifier::new(which),
786            ducker: TransientDucker::new(),
787        }
788    }
789}
790
791/// Inputs to one full §5.7.7.5 channel-pair synthesis pass.
792pub struct AcplCpeFrame<'a> {
793    /// `x0[ts][sb]` — left / first-channel QMF input matrix
794    /// (`num_qmf_timeslots` slots × 64 subbands). For ASPX_ACPL_2 the
795    /// `x1` channel is absent (caller passes None).
796    pub x0: &'a [[(f32, f32); NUM_QMF_SUBBANDS]],
797    /// `x1[ts][sb]` — right / second-channel QMF input matrix. Some()
798    /// for ASPX_ACPL_1, None for ASPX_ACPL_2.
799    pub x1: Option<&'a [[(f32, f32); NUM_QMF_SUBBANDS]]>,
800    /// `acpl_alpha_dq[pset][pb]` recovered values.
801    pub alpha_dq: &'a [Vec<f32>],
802    /// `acpl_beta_dq[pset][pb]` recovered values.
803    pub beta_dq: &'a [Vec<f32>],
804    /// `acpl_num_param_bands`.
805    pub num_param_bands: u32,
806    /// `acpl_qmf_band` from Table 59 — first subband at which the
807    /// mixed-with-decorrelator path takes over from the M/S split.
808    /// (For multichannel paths the spec sets this to 0.)
809    pub acpl_qmf_band: u32,
810    /// Steep interpolation flag.
811    pub steep: bool,
812    /// `acpl_param_timeslot[pset]` for the steep mode.
813    pub param_timeslots: &'a [u8],
814}
815
816/// Output matrices from [`acpl_module`] — `(z0, z1)` per Pseudocode 116.
817pub struct AcplCpeOutput {
818    pub z0: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>,
819    pub z1: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>,
820}
821
822/// `Pseudocode 116 ACplModule(acpl_alpha, acpl_beta, num_pset, x0, x1, y)`
823/// — the two-channel A-CPL synthesis.
824///
825/// `y[ts][sb]` is the post-ducker decorrelated output of `x0` (provided
826/// by [`run_pseudocode_115_pair`] below); `x1` may be all-zero for the
827/// `ASPX_ACPL_2` mode (single-channel input).
828pub fn acpl_module(
829    frame: &AcplCpeFrame<'_>,
830    y: &[[(f32, f32); NUM_QMF_SUBBANDS]],
831) -> AcplCpeOutput {
832    let num_ts = frame.x0.len();
833    debug_assert_eq!(y.len(), num_ts);
834    let num_pset = frame.alpha_dq.len() as u32;
835    let alpha_sb = expand_pb_to_sb(frame.alpha_dq, frame.num_param_bands);
836    let beta_sb = expand_pb_to_sb(frame.beta_dq, frame.num_param_bands);
837    // Default prev to zeros for the first frame.
838    let prev_alpha = vec![0.0f32; NUM_QMF_SUBBANDS];
839    let prev_beta = vec![0.0f32; NUM_QMF_SUBBANDS];
840    let alpha_inp = InterpInputs {
841        by_pset: &alpha_sb,
842        prev: &prev_alpha,
843    };
844    let beta_inp = InterpInputs {
845        by_pset: &beta_sb,
846        prev: &prev_beta,
847    };
848
849    let zero_col = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
850    let mut z0_out = Vec::with_capacity(num_ts);
851    let mut z1_out = Vec::with_capacity(num_ts);
852    for ts in 0..num_ts {
853        let x0_col = frame.x0[ts];
854        let x1_col = match frame.x1 {
855            Some(matrix) => matrix[ts],
856            None => zero_col,
857        };
858        let y_col = y[ts];
859        let mut z0_col = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
860        let mut z1_col = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
861        for sb in 0..NUM_QMF_SUBBANDS as u32 {
862            let interp_a = interpolate(
863                &alpha_inp,
864                num_pset,
865                sb,
866                ts as u32,
867                num_ts as u32,
868                frame.steep,
869                frame.param_timeslots,
870            );
871            let interp_b = interpolate(
872                &beta_inp,
873                num_pset,
874                sb,
875                ts as u32,
876                num_ts as u32,
877                frame.steep,
878                frame.param_timeslots,
879            );
880            let sb_i = sb as usize;
881            if sb < frame.acpl_qmf_band {
882                let (x0r, x0i) = x0_col[sb_i];
883                let (x1r, x1i) = x1_col[sb_i];
884                z0_col[sb_i] = (0.5 * (x0r + x1r), 0.5 * (x0i + x1i));
885                z1_col[sb_i] = (0.5 * (x0r - x1r), 0.5 * (x0i - x1i));
886            } else {
887                let (x0r, x0i) = x0_col[sb_i];
888                let (yr, yi) = y_col[sb_i];
889                let plus_a = 1.0 + interp_a;
890                let minus_a = 1.0 - interp_a;
891                z0_col[sb_i] = (
892                    0.5 * (x0r * plus_a + yr * interp_b),
893                    0.5 * (x0i * plus_a + yi * interp_b),
894                );
895                z1_col[sb_i] = (
896                    0.5 * (x0r * minus_a - yr * interp_b),
897                    0.5 * (x0i * minus_a - yi * interp_b),
898                );
899            }
900        }
901        z0_out.push(z0_col);
902        z1_out.push(z1_col);
903    }
904    AcplCpeOutput {
905        z0: z0_out,
906        z1: z1_out,
907    }
908}
909
910/// Run the complete §5.7.7.5 channel-pair element pipeline
911/// (Pseudocode 115): scale inputs by 2, run the decorrelator + ducker
912/// to get `y0`, then call [`acpl_module`].
913///
914/// `state` carries the `D0` decorrelator + ducker between calls (so
915/// the IIR delay-line state survives across frames).
916pub fn run_pseudocode_115_pair(state: &mut AcplCpeState, frame: AcplCpeFrame<'_>) -> AcplCpeOutput {
917    // x0in = 2 * x0
918    let num_ts = frame.x0.len();
919    let mut x0in = Vec::with_capacity(num_ts);
920    for ts in 0..num_ts {
921        let mut col = frame.x0[ts];
922        for sb in 0..NUM_QMF_SUBBANDS {
923            col[sb].0 *= 2.0;
924            col[sb].1 *= 2.0;
925        }
926        x0in.push(col);
927    }
928    // u0 = inputSignalModification(x0in)  (D0)
929    let mut u0 = Vec::with_capacity(num_ts);
930    for ts in 0..num_ts {
931        let mut col = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
932        for sb in 0..NUM_QMF_SUBBANDS as u32 {
933            col[sb as usize] = state.decorrelator.process_sample(sb, x0in[ts][sb as usize]);
934        }
935        u0.push(col);
936    }
937    // y0 = applyTransientDucker(u0)
938    let mut y0 = Vec::with_capacity(num_ts);
939    for ts in 0..num_ts {
940        let p_energy = compute_p_energy(&u0[ts], frame.num_param_bands);
941        let duck = state.ducker.update(&p_energy);
942        y0.push(apply_transient_ducker(
943            &u0[ts],
944            &duck,
945            frame.num_param_bands,
946        ));
947    }
948    // Call acpl_module with x0in (note the spec: ACplModule receives
949    // x0in, not raw x0).
950    let mut x1in_owned: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>;
951    let inner_x1: Option<&[[(f32, f32); NUM_QMF_SUBBANDS]]> = if let Some(x1) = frame.x1 {
952        x1in_owned = Vec::with_capacity(num_ts);
953        for ts in 0..num_ts {
954            let mut col = x1[ts];
955            for sb in 0..NUM_QMF_SUBBANDS {
956                col[sb].0 *= 2.0;
957                col[sb].1 *= 2.0;
958            }
959            x1in_owned.push(col);
960        }
961        Some(x1in_owned.as_slice())
962    } else {
963        None
964    };
965    let inner_frame = AcplCpeFrame {
966        x0: &x0in,
967        x1: inner_x1,
968        alpha_dq: frame.alpha_dq,
969        beta_dq: frame.beta_dq,
970        num_param_bands: frame.num_param_bands,
971        acpl_qmf_band: frame.acpl_qmf_band,
972        steep: frame.steep,
973        param_timeslots: frame.param_timeslots,
974    };
975    let out = acpl_module(&inner_frame, &y0);
976    // Update prev arrays per Pseudocode 110 (last param set's expanded
977    // sb values).
978    if !frame.alpha_dq.is_empty() {
979        let last_idx = frame.alpha_dq.len() - 1;
980        let alpha_sb = expand_pb_to_sb(frame.alpha_dq, frame.num_param_bands);
981        let beta_sb = expand_pb_to_sb(frame.beta_dq, frame.num_param_bands);
982        update_param_prev(&mut state.alpha_prev_sb, &alpha_sb[last_idx]);
983        update_param_prev(&mut state.beta_prev_sb, &beta_sb[last_idx]);
984    }
985    out
986}
987
988// =====================================================================
989// §5.7.7.6.2 Pseudocodes 118 / 119 — ASPX_ACPL_3 multichannel synthesis
990// =====================================================================
991
992/// Per-channel QMF matrix shape used throughout §5.7.7.6.2: a vector of
993/// per-timeslot `[ (re, im); NUM_QMF_SUBBANDS ]` columns.
994pub type AcplQmfMatrix = Vec<[(f32, f32); NUM_QMF_SUBBANDS]>;
995
996/// `Transform(g1, g2, num_pset, x0, x1)` per §5.7.7.6.2 Pseudocode 119.
997///
998/// Linearly mixes the two A-CPL carrier channels (`x0`, `x1`) by the
999/// interpolated gamma matrices `g1`, `g2`:
1000///
1001/// ```text
1002///   v[ts][sb] = x0[ts][sb] * interp_g1[ts][sb]
1003///             + x1[ts][sb] * interp_g2[ts][sb]
1004/// ```
1005///
1006/// `g1_pb` / `g2_pb` are per-`(pset, pb)` matrices; we fan them out to
1007/// per-subband via [`expand_pb_to_sb`] and then the §5.7.7.3 [`interpolate`]
1008/// call walks them across timeslots. `prev_g1` / `prev_g2` carry the
1009/// previous frame's last-set `[sb]` row (zero on the first frame, per the
1010/// spec).
1011///
1012/// The output is `[ts][sb]` shaped, matching the rest of the §5.7.7
1013/// per-slot interfaces.
1014#[allow(clippy::too_many_arguments)]
1015pub fn transform(
1016    x0: &[[(f32, f32); NUM_QMF_SUBBANDS]],
1017    x1: &[[(f32, f32); NUM_QMF_SUBBANDS]],
1018    g1_pb: &[Vec<f32>],
1019    g2_pb: &[Vec<f32>],
1020    num_param_bands: u32,
1021    prev_g1: &[f32],
1022    prev_g2: &[f32],
1023    steep: bool,
1024    param_timeslots: &[u8],
1025) -> Vec<[(f32, f32); NUM_QMF_SUBBANDS]> {
1026    let num_ts = x0.len();
1027    debug_assert_eq!(x1.len(), num_ts);
1028    let num_pset = g1_pb.len() as u32;
1029    let g1_sb = expand_pb_to_sb(g1_pb, num_param_bands);
1030    let g2_sb = expand_pb_to_sb(g2_pb, num_param_bands);
1031    let g1_inp = InterpInputs {
1032        by_pset: &g1_sb,
1033        prev: prev_g1,
1034    };
1035    let g2_inp = InterpInputs {
1036        by_pset: &g2_sb,
1037        prev: prev_g2,
1038    };
1039    let mut out = Vec::with_capacity(num_ts);
1040    for ts in 0..num_ts {
1041        let mut v_col = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
1042        for sb in 0..NUM_QMF_SUBBANDS as u32 {
1043            let g1 = interpolate(
1044                &g1_inp,
1045                num_pset,
1046                sb,
1047                ts as u32,
1048                num_ts as u32,
1049                steep,
1050                param_timeslots,
1051            );
1052            let g2 = interpolate(
1053                &g2_inp,
1054                num_pset,
1055                sb,
1056                ts as u32,
1057                num_ts as u32,
1058                steep,
1059                param_timeslots,
1060            );
1061            let (x0r, x0i) = x0[ts][sb as usize];
1062            let (x1r, x1i) = x1[ts][sb as usize];
1063            v_col[sb as usize] = (x0r * g1 + x1r * g2, x0i * g1 + x1i * g2);
1064        }
1065        out.push(v_col);
1066    }
1067    out
1068}
1069
1070/// `ACplModule2(g1, g2, a, b, num_pset, x0, x1, y)` per §5.7.7.6.2
1071/// Pseudocode 119.
1072///
1073/// Builds the (z0, z1) channel pair from the two A-CPL carrier inputs,
1074/// the gamma + alpha + beta parameter matrices, and the decorrelator
1075/// output `y`:
1076///
1077/// ```text
1078///   z0 = 0.5*(x0*(g1+g1*a) + x1*(g2+g2*a) + y*b)
1079///   z1 = 0.5*(x0*(g1-g1*a) + x1*(g2-g2*a) - y*b)
1080/// ```
1081///
1082/// The interpolations are taken on the full `g1`, `g2`, `g1*a`, `g2*a`
1083/// and `b` per-`pb` matrices (computed from the dequantised arrays
1084/// before the call). Per the spec the interpolations are computed on
1085/// the products `g*a` (not on `g` and `a` separately) because that's
1086/// the point at which time-interpolation must be evaluated.
1087///
1088/// `prev_*` are the previous-frame `[sb]` rows for each interpolated
1089/// matrix (zero on the first frame).
1090#[allow(clippy::too_many_arguments)]
1091pub fn acpl_module2(
1092    x0: &[[(f32, f32); NUM_QMF_SUBBANDS]],
1093    x1: &[[(f32, f32); NUM_QMF_SUBBANDS]],
1094    y: &[[(f32, f32); NUM_QMF_SUBBANDS]],
1095    g1_pb: &[Vec<f32>],
1096    g2_pb: &[Vec<f32>],
1097    g1a_pb: &[Vec<f32>],
1098    g2a_pb: &[Vec<f32>],
1099    b_pb: &[Vec<f32>],
1100    num_param_bands: u32,
1101    steep: bool,
1102    param_timeslots: &[u8],
1103) -> (AcplQmfMatrix, AcplQmfMatrix) {
1104    let num_ts = x0.len();
1105    debug_assert_eq!(x1.len(), num_ts);
1106    debug_assert_eq!(y.len(), num_ts);
1107    let num_pset = g1_pb.len() as u32;
1108    let g1_sb = expand_pb_to_sb(g1_pb, num_param_bands);
1109    let g2_sb = expand_pb_to_sb(g2_pb, num_param_bands);
1110    let g1a_sb = expand_pb_to_sb(g1a_pb, num_param_bands);
1111    let g2a_sb = expand_pb_to_sb(g2a_pb, num_param_bands);
1112    let b_sb = expand_pb_to_sb(b_pb, num_param_bands);
1113    let zero_prev = vec![0.0f32; NUM_QMF_SUBBANDS];
1114    let g1_inp = InterpInputs {
1115        by_pset: &g1_sb,
1116        prev: &zero_prev,
1117    };
1118    let g2_inp = InterpInputs {
1119        by_pset: &g2_sb,
1120        prev: &zero_prev,
1121    };
1122    let g1a_inp = InterpInputs {
1123        by_pset: &g1a_sb,
1124        prev: &zero_prev,
1125    };
1126    let g2a_inp = InterpInputs {
1127        by_pset: &g2a_sb,
1128        prev: &zero_prev,
1129    };
1130    let b_inp = InterpInputs {
1131        by_pset: &b_sb,
1132        prev: &zero_prev,
1133    };
1134
1135    let mut z0_out = Vec::with_capacity(num_ts);
1136    let mut z1_out = Vec::with_capacity(num_ts);
1137    for ts in 0..num_ts {
1138        let mut z0_col = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
1139        let mut z1_col = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
1140        for sb in 0..NUM_QMF_SUBBANDS as u32 {
1141            let g1 = interpolate(
1142                &g1_inp,
1143                num_pset,
1144                sb,
1145                ts as u32,
1146                num_ts as u32,
1147                steep,
1148                param_timeslots,
1149            );
1150            let g2 = interpolate(
1151                &g2_inp,
1152                num_pset,
1153                sb,
1154                ts as u32,
1155                num_ts as u32,
1156                steep,
1157                param_timeslots,
1158            );
1159            let g1a = interpolate(
1160                &g1a_inp,
1161                num_pset,
1162                sb,
1163                ts as u32,
1164                num_ts as u32,
1165                steep,
1166                param_timeslots,
1167            );
1168            let g2a = interpolate(
1169                &g2a_inp,
1170                num_pset,
1171                sb,
1172                ts as u32,
1173                num_ts as u32,
1174                steep,
1175                param_timeslots,
1176            );
1177            let b = interpolate(
1178                &b_inp,
1179                num_pset,
1180                sb,
1181                ts as u32,
1182                num_ts as u32,
1183                steep,
1184                param_timeslots,
1185            );
1186            let sb_i = sb as usize;
1187            let (x0r, x0i) = x0[ts][sb_i];
1188            let (x1r, x1i) = x1[ts][sb_i];
1189            let (yr, yi) = y[ts][sb_i];
1190            z0_col[sb_i] = (
1191                0.5 * (x0r * (g1 + g1a) + x1r * (g2 + g2a) + yr * b),
1192                0.5 * (x0i * (g1 + g1a) + x1i * (g2 + g2a) + yi * b),
1193            );
1194            z1_col[sb_i] = (
1195                0.5 * (x0r * (g1 - g1a) + x1r * (g2 - g2a) - yr * b),
1196                0.5 * (x0i * (g1 - g1a) + x1i * (g2 - g2a) - yi * b),
1197            );
1198        }
1199        z0_out.push(z0_col);
1200        z1_out.push(z1_col);
1201    }
1202    (z0_out, z1_out)
1203}
1204
1205/// `ACplModule3(b3, a, num_pset, z0, z1, y2)` per §5.7.7.6.2
1206/// Pseudocode 119.
1207///
1208/// Adds a cross-residual decorrelator term to an existing `(z0, z1)`
1209/// pair using `beta3` and the alpha matrix:
1210///
1211/// ```text
1212///   z0 += 0.25 * y2 * (b3 + b3*a)
1213///   z1 += 0.25 * y2 * (b3 - b3*a)
1214/// ```
1215///
1216/// The dequantised `b3_pb` and `b3a_pb` per-`(pset, pb)` matrices are
1217/// fanned out to subbands via [`expand_pb_to_sb`] and run through
1218/// [`interpolate`] across timeslots. The `(z0, z1)` slices are mutated
1219/// in-place to match the spec's `z0[ts][sb] += ...` form.
1220#[allow(clippy::too_many_arguments)]
1221pub fn acpl_module3(
1222    z0: &mut [[(f32, f32); NUM_QMF_SUBBANDS]],
1223    z1: &mut [[(f32, f32); NUM_QMF_SUBBANDS]],
1224    y2: &[[(f32, f32); NUM_QMF_SUBBANDS]],
1225    b3_pb: &[Vec<f32>],
1226    b3a_pb: &[Vec<f32>],
1227    num_param_bands: u32,
1228    steep: bool,
1229    param_timeslots: &[u8],
1230) {
1231    let num_ts = z0.len();
1232    debug_assert_eq!(z1.len(), num_ts);
1233    debug_assert_eq!(y2.len(), num_ts);
1234    let num_pset = b3_pb.len() as u32;
1235    let b3_sb = expand_pb_to_sb(b3_pb, num_param_bands);
1236    let b3a_sb = expand_pb_to_sb(b3a_pb, num_param_bands);
1237    let zero_prev = vec![0.0f32; NUM_QMF_SUBBANDS];
1238    let b3_inp = InterpInputs {
1239        by_pset: &b3_sb,
1240        prev: &zero_prev,
1241    };
1242    let b3a_inp = InterpInputs {
1243        by_pset: &b3a_sb,
1244        prev: &zero_prev,
1245    };
1246    for ts in 0..num_ts {
1247        for sb in 0..NUM_QMF_SUBBANDS as u32 {
1248            let b3 = interpolate(
1249                &b3_inp,
1250                num_pset,
1251                sb,
1252                ts as u32,
1253                num_ts as u32,
1254                steep,
1255                param_timeslots,
1256            );
1257            let b3a = interpolate(
1258                &b3a_inp,
1259                num_pset,
1260                sb,
1261                ts as u32,
1262                num_ts as u32,
1263                steep,
1264                param_timeslots,
1265            );
1266            let sb_i = sb as usize;
1267            let (yr, yi) = y2[ts][sb_i];
1268            z0[ts][sb_i].0 += 0.25 * yr * (b3 + b3a);
1269            z0[ts][sb_i].1 += 0.25 * yi * (b3 + b3a);
1270            z1[ts][sb_i].0 += 0.25 * yr * (b3 - b3a);
1271            z1[ts][sb_i].1 += 0.25 * yi * (b3 - b3a);
1272        }
1273    }
1274}
1275
1276/// Helper: per-`(pset, pb)` element-wise multiply of two per-band
1277/// matrices. Returned matrix has the same shape as `a`. For mismatched
1278/// inner-row lengths the shorter wins.
1279fn pb_matrix_mul(a: &[Vec<f32>], b: &[Vec<f32>]) -> Vec<Vec<f32>> {
1280    debug_assert_eq!(a.len(), b.len());
1281    a.iter()
1282        .zip(b.iter())
1283        .map(|(ar, br)| {
1284            let n = ar.len().min(br.len());
1285            let mut row = Vec::with_capacity(n);
1286            for i in 0..n {
1287                row.push(ar[i] * br[i]);
1288            }
1289            row
1290        })
1291        .collect()
1292}
1293
1294/// Helper: per-`(pset, pb)` element-wise sum of three per-band
1295/// matrices. Used to build `g1+g3+g5` and `g2+g4+g6` for the third
1296/// `Transform()` call in Pseudocode 118.
1297fn pb_matrix_sum3(a: &[Vec<f32>], b: &[Vec<f32>], c: &[Vec<f32>]) -> Vec<Vec<f32>> {
1298    debug_assert_eq!(a.len(), b.len());
1299    debug_assert_eq!(a.len(), c.len());
1300    a.iter()
1301        .zip(b.iter())
1302        .zip(c.iter())
1303        .map(|((ar, br), cr)| {
1304            let n = ar.len().min(br.len()).min(cr.len());
1305            let mut row = Vec::with_capacity(n);
1306            for i in 0..n {
1307                row.push(ar[i] + br[i] + cr[i]);
1308            }
1309            row
1310        })
1311        .collect()
1312}
1313
1314/// Helper: scalar-multiply each element of a `(pset, pb)` matrix.
1315fn pb_matrix_scale(a: &[Vec<f32>], s: f32) -> Vec<Vec<f32>> {
1316    a.iter()
1317        .map(|row| row.iter().map(|&v| v * s).collect())
1318        .collect()
1319}
1320
1321/// Persistent state for the §5.7.7.6.2 ASPX_ACPL_3 multichannel
1322/// pipeline: three parallel `D0`/`D1`/`D2` decorrelator + ducker pairs
1323/// (one per `Transform()` output) plus the running `prev` matrices for
1324/// the gamma interpolations.
1325#[derive(Debug, Clone)]
1326pub struct AcplMchState {
1327    pub d0: InputSignalModifier,
1328    pub d1: InputSignalModifier,
1329    pub d2: InputSignalModifier,
1330    pub ducker0: TransientDucker,
1331    pub ducker1: TransientDucker,
1332    pub ducker2: TransientDucker,
1333    /// `acpl_g1_prev[sb]` — last-frame's per-sb gamma1 row.
1334    pub g1_prev_sb: Vec<f32>,
1335    pub g2_prev_sb: Vec<f32>,
1336    pub g3_prev_sb: Vec<f32>,
1337    pub g4_prev_sb: Vec<f32>,
1338}
1339
1340impl AcplMchState {
1341    pub fn new() -> Self {
1342        Self {
1343            d0: InputSignalModifier::new(DecorrelatorId::D0),
1344            d1: InputSignalModifier::new(DecorrelatorId::D1),
1345            d2: InputSignalModifier::new(DecorrelatorId::D2),
1346            ducker0: TransientDucker::new(),
1347            ducker1: TransientDucker::new(),
1348            ducker2: TransientDucker::new(),
1349            g1_prev_sb: vec![0.0; NUM_QMF_SUBBANDS],
1350            g2_prev_sb: vec![0.0; NUM_QMF_SUBBANDS],
1351            g3_prev_sb: vec![0.0; NUM_QMF_SUBBANDS],
1352            g4_prev_sb: vec![0.0; NUM_QMF_SUBBANDS],
1353        }
1354    }
1355}
1356
1357impl Default for AcplMchState {
1358    fn default() -> Self {
1359        Self::new()
1360    }
1361}
1362
1363/// Inputs to one full §5.7.7.6.2 ASPX_ACPL_3 multichannel synthesis pass.
1364///
1365/// All matrices are per-`(pset, pb)`. The dequantisation pipeline must
1366/// have already converted the Huffman-decoded indices via
1367/// [`dequantize_alpha_beta`] / [`dequantize_beta3`] / [`dequantize_gamma`].
1368pub struct AcplMchFrame<'a> {
1369    /// `x0[ts][sb]` — first A-CPL carrier (left/L for ASPX_ACPL_3).
1370    pub x0: &'a [[(f32, f32); NUM_QMF_SUBBANDS]],
1371    /// `x1[ts][sb]` — second A-CPL carrier (right/R for ASPX_ACPL_3).
1372    pub x1: &'a [[(f32, f32); NUM_QMF_SUBBANDS]],
1373    /// `x2[ts][sb]` — centre channel passthrough.
1374    pub x2: &'a [[(f32, f32); NUM_QMF_SUBBANDS]],
1375    /// `acpl_alpha_1_dq[pset][pb]` (left side).
1376    pub alpha_1_dq: &'a [Vec<f32>],
1377    /// `acpl_alpha_2_dq[pset][pb]` (right side).
1378    pub alpha_2_dq: &'a [Vec<f32>],
1379    /// `acpl_beta_1_dq[pset][pb]`.
1380    pub beta_1_dq: &'a [Vec<f32>],
1381    /// `acpl_beta_2_dq[pset][pb]`.
1382    pub beta_2_dq: &'a [Vec<f32>],
1383    /// `acpl_beta_3_dq[pset][pb]` — the cross-residual term used by
1384    /// `ACplModule3`.
1385    pub beta_3_dq: &'a [Vec<f32>],
1386    /// `g1_dq[pset][pb]` .. `g6_dq[pset][pb]` per §5.7.7.7 Tables 207-208.
1387    pub g1_dq: &'a [Vec<f32>],
1388    pub g2_dq: &'a [Vec<f32>],
1389    pub g3_dq: &'a [Vec<f32>],
1390    pub g4_dq: &'a [Vec<f32>],
1391    pub g5_dq: &'a [Vec<f32>],
1392    pub g6_dq: &'a [Vec<f32>],
1393    pub num_param_bands: u32,
1394    pub steep: bool,
1395    pub param_timeslots: &'a [u8],
1396}
1397
1398/// Output channels from [`run_pseudocode_118_5x`] — `(L, R, C, Ls, Rs)`
1399/// indexed as `(z0, z2, z4, z1, z3)` in the spec.
1400pub struct AcplMchOutput {
1401    pub z0: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>, // L
1402    pub z2: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>, // R
1403    pub z4: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>, // C
1404    pub z1: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>, // Ls
1405    pub z3: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>, // Rs
1406}
1407
1408/// Run the full §5.7.7.6.2 Pseudocode 118 ASPX_ACPL_3 multichannel
1409/// pipeline. Produces the five output channels (L, Ls, R, Rs, C) from
1410/// the two A-CPL carriers, the centre channel and the gamma+alpha+beta
1411/// matrices.
1412///
1413/// Pipeline (verbatim from Pseudocode 118):
1414///
1415/// 1. `x0in = x0*(1+2*sqrt(0.5))`, `x1in = x1*(1+2*sqrt(0.5))`.
1416/// 2. `v1 = Transform(g1, g2, x0in, x1in)`,
1417///    `v2 = Transform(g3, g4, x0in, x1in)`,
1418///    `v3 = Transform(g1+g3+g5, g2+g4+g6, x0in, x1in)`.
1419/// 3. `u0 = D0(v1)`, `u1 = D1(v2)`, `u2 = D2(v3)`.
1420/// 4. `y0 = ducker(u0)`, `y1 = ducker(u1)`, `y2 = ducker(u2)`.
1421/// 5. `(z0, z1) = ACplModule2(g1, g2, alpha_1, beta_1, x0in, x1in, y0)`.
1422/// 6. `(z2, z3) = ACplModule2(g3, g4, alpha_2, beta_2, x0in, x1in, y1)`.
1423/// 7. `(z4, z5) = ACplModule2(g5, g6, 1, 0, x0in, x1in, 0)`.
1424/// 8. `(z0, z1) = ACplModule3(beta_3, alpha_1, z0, z1, y2)`.
1425/// 9. `(z2, z3) = ACplModule3(beta_3, alpha_2, z2, z3, y2)`.
1426/// 10. `(z4, z5) = ACplModule3(-beta_3, 1, z4, z5, y2)`.
1427/// 11. `z1 *= sqrt(2)`, `z3 *= sqrt(2)`, `z4 *= sqrt(2)`.
1428/// 12. `z4 = x2` (note: per the spec text, z4 is initialised from
1429///     `ACplModule2(g5, g6, ..)` and then has the ACplModule3 correction
1430///     and `*sqrt(2)` applied — but the surrounding text in §5.7.7.6.2
1431///     reads "z4 = x2" outside the pseudocode body. We follow the
1432///     pseudocode literally: the centre channel is the synthesised z4;
1433///     a caller wanting the spec's "z4 = x2" passthrough can override
1434///     [`AcplMchOutput::z4`] after the fact).
1435pub fn run_pseudocode_118_5x(state: &mut AcplMchState, frame: AcplMchFrame<'_>) -> AcplMchOutput {
1436    let num_ts = frame.x0.len();
1437    debug_assert_eq!(frame.x1.len(), num_ts);
1438    debug_assert_eq!(frame.x2.len(), num_ts);
1439
1440    // Step 1: x0in = x0 * (1 + 2*sqrt(0.5)), x1in = x1 * (1 + 2*sqrt(0.5)).
1441    // 1 + 2*sqrt(0.5) = 1 + sqrt(2).
1442    let scale = 1.0 + 2.0 * (0.5f32).sqrt();
1443    let scale_x = |x: &[[(f32, f32); NUM_QMF_SUBBANDS]]| -> Vec<[(f32, f32); NUM_QMF_SUBBANDS]> {
1444        x.iter()
1445            .map(|col| {
1446                let mut out = *col;
1447                for sb in 0..NUM_QMF_SUBBANDS {
1448                    out[sb].0 *= scale;
1449                    out[sb].1 *= scale;
1450                }
1451                out
1452            })
1453            .collect()
1454    };
1455    let x0in = scale_x(frame.x0);
1456    let x1in = scale_x(frame.x1);
1457
1458    // Step 2: build the three Transform() outputs.
1459    let g_sum_1 = pb_matrix_sum3(frame.g1_dq, frame.g3_dq, frame.g5_dq);
1460    let g_sum_2 = pb_matrix_sum3(frame.g2_dq, frame.g4_dq, frame.g6_dq);
1461    let v1 = transform(
1462        &x0in,
1463        &x1in,
1464        frame.g1_dq,
1465        frame.g2_dq,
1466        frame.num_param_bands,
1467        &state.g1_prev_sb,
1468        &state.g2_prev_sb,
1469        frame.steep,
1470        frame.param_timeslots,
1471    );
1472    let v2 = transform(
1473        &x0in,
1474        &x1in,
1475        frame.g3_dq,
1476        frame.g4_dq,
1477        frame.num_param_bands,
1478        &state.g3_prev_sb,
1479        &state.g4_prev_sb,
1480        frame.steep,
1481        frame.param_timeslots,
1482    );
1483    let v3 = transform(
1484        &x0in,
1485        &x1in,
1486        &g_sum_1,
1487        &g_sum_2,
1488        frame.num_param_bands,
1489        &vec![0.0; NUM_QMF_SUBBANDS],
1490        &vec![0.0; NUM_QMF_SUBBANDS],
1491        frame.steep,
1492        frame.param_timeslots,
1493    );
1494
1495    // Step 3: u_x = decorrelator_x(v_x).
1496    let mut u0 = Vec::with_capacity(num_ts);
1497    let mut u1 = Vec::with_capacity(num_ts);
1498    let mut u2 = Vec::with_capacity(num_ts);
1499    for ts in 0..num_ts {
1500        let mut col0 = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
1501        let mut col1 = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
1502        let mut col2 = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
1503        for sb in 0..NUM_QMF_SUBBANDS as u32 {
1504            col0[sb as usize] = state.d0.process_sample(sb, v1[ts][sb as usize]);
1505            col1[sb as usize] = state.d1.process_sample(sb, v2[ts][sb as usize]);
1506            col2[sb as usize] = state.d2.process_sample(sb, v3[ts][sb as usize]);
1507        }
1508        u0.push(col0);
1509        u1.push(col1);
1510        u2.push(col2);
1511    }
1512
1513    // Step 4: y_x = ducker(u_x).
1514    let apply_ducker = |u: &[[(f32, f32); NUM_QMF_SUBBANDS]],
1515                        ducker: &mut TransientDucker|
1516     -> Vec<[(f32, f32); NUM_QMF_SUBBANDS]> {
1517        u.iter()
1518            .map(|col| {
1519                let p_energy = compute_p_energy(col, frame.num_param_bands);
1520                let duck = ducker.update(&p_energy);
1521                apply_transient_ducker(col, &duck, frame.num_param_bands)
1522            })
1523            .collect()
1524    };
1525    let y0 = apply_ducker(&u0, &mut state.ducker0);
1526    let y1 = apply_ducker(&u1, &mut state.ducker1);
1527    let y2 = apply_ducker(&u2, &mut state.ducker2);
1528
1529    // Step 5: (z0, z1) = ACplModule2(g1, g2, alpha_1, beta_1, x0in, x1in, y0).
1530    // Per Pseudocode 119 the (g1*a) / (g2*a) interpolations sample the
1531    // *product* matrices — we precompute them at parameter-band granularity.
1532    let g1_a1 = pb_matrix_mul(frame.g1_dq, frame.alpha_1_dq);
1533    let g2_a1 = pb_matrix_mul(frame.g2_dq, frame.alpha_1_dq);
1534    let (mut z0, mut z1) = acpl_module2(
1535        &x0in,
1536        &x1in,
1537        &y0,
1538        frame.g1_dq,
1539        frame.g2_dq,
1540        &g1_a1,
1541        &g2_a1,
1542        frame.beta_1_dq,
1543        frame.num_param_bands,
1544        frame.steep,
1545        frame.param_timeslots,
1546    );
1547
1548    // Step 6: (z2, z3) = ACplModule2(g3, g4, alpha_2, beta_2, x0in, x1in, y1).
1549    let g3_a2 = pb_matrix_mul(frame.g3_dq, frame.alpha_2_dq);
1550    let g4_a2 = pb_matrix_mul(frame.g4_dq, frame.alpha_2_dq);
1551    let (mut z2, mut z3) = acpl_module2(
1552        &x0in,
1553        &x1in,
1554        &y1,
1555        frame.g3_dq,
1556        frame.g4_dq,
1557        &g3_a2,
1558        &g4_a2,
1559        frame.beta_2_dq,
1560        frame.num_param_bands,
1561        frame.steep,
1562        frame.param_timeslots,
1563    );
1564
1565    // Step 7: (z4, z5) = ACplModule2(g5, g6, 1, 0, x0in, x1in, 0).
1566    // a == 1 → g*a == g; b == 0 → no decorrelator term; y == 0 too.
1567    let zero_y = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
1568    // β = 0 across all bands. The shape follows the gamma matrices.
1569    let zero_pb: Vec<Vec<f32>> = frame
1570        .g5_dq
1571        .iter()
1572        .map(|row| vec![0.0f32; row.len()])
1573        .collect();
1574    let (mut z4, _z5) = acpl_module2(
1575        &x0in,
1576        &x1in,
1577        &zero_y,
1578        frame.g5_dq,
1579        frame.g6_dq,
1580        frame.g5_dq, // g5 * 1
1581        frame.g6_dq, // g6 * 1
1582        &zero_pb,
1583        frame.num_param_bands,
1584        frame.steep,
1585        frame.param_timeslots,
1586    );
1587    // _z5 is a temporary per the spec note ("Note that z5 is used as a
1588    // temporary variable only and does not constitute an output channel.").
1589
1590    // Step 8: (z0, z1) += ACplModule3(beta_3, alpha_1, z0, z1, y2).
1591    let b3_a1 = pb_matrix_mul(frame.beta_3_dq, frame.alpha_1_dq);
1592    acpl_module3(
1593        &mut z0,
1594        &mut z1,
1595        &y2,
1596        frame.beta_3_dq,
1597        &b3_a1,
1598        frame.num_param_bands,
1599        frame.steep,
1600        frame.param_timeslots,
1601    );
1602
1603    // Step 9: (z2, z3) += ACplModule3(beta_3, alpha_2, z2, z3, y2).
1604    let b3_a2 = pb_matrix_mul(frame.beta_3_dq, frame.alpha_2_dq);
1605    acpl_module3(
1606        &mut z2,
1607        &mut z3,
1608        &y2,
1609        frame.beta_3_dq,
1610        &b3_a2,
1611        frame.num_param_bands,
1612        frame.steep,
1613        frame.param_timeslots,
1614    );
1615
1616    // Step 10: (z4, z5) += ACplModule3(-beta_3, 1, z4, z5, y2). a == 1 →
1617    // beta3*a == beta3 (i.e. b3a == b3 with a sign flip on b3 — but the
1618    // spec writes -b3 for this row). So b3 is negated and b3a == -b3 too.
1619    let neg_b3 = pb_matrix_scale(frame.beta_3_dq, -1.0);
1620    let neg_b3_a = pb_matrix_scale(frame.beta_3_dq, -1.0); // a == 1 → -b3*1 = -b3.
1621    let mut z5_dummy = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
1622    acpl_module3(
1623        &mut z4,
1624        &mut z5_dummy,
1625        &y2,
1626        &neg_b3,
1627        &neg_b3_a,
1628        frame.num_param_bands,
1629        frame.steep,
1630        frame.param_timeslots,
1631    );
1632
1633    // Step 11: z1 *= sqrt(2), z3 *= sqrt(2), z4 *= sqrt(2).
1634    let sq2 = (2.0f32).sqrt();
1635    let scale_inplace = |z: &mut [[(f32, f32); NUM_QMF_SUBBANDS]], s: f32| {
1636        for col in z.iter_mut() {
1637            for sb in 0..NUM_QMF_SUBBANDS {
1638                col[sb].0 *= s;
1639                col[sb].1 *= s;
1640            }
1641        }
1642    };
1643    scale_inplace(&mut z1, sq2);
1644    scale_inplace(&mut z3, sq2);
1645    scale_inplace(&mut z4, sq2);
1646
1647    // Update state's prev arrays from the last param set's expanded
1648    // [sb] rows (gammas only — alpha/beta state lives elsewhere if a
1649    // caller wants to chain it).
1650    let update_prev = |prev: &mut Vec<f32>, src_pb: &[Vec<f32>]| {
1651        if !src_pb.is_empty() {
1652            let last = &src_pb[src_pb.len() - 1];
1653            let sb = expand_pb_to_sb(std::slice::from_ref(last), frame.num_param_bands);
1654            update_param_prev(prev, &sb[0]);
1655        }
1656    };
1657    update_prev(&mut state.g1_prev_sb, frame.g1_dq);
1658    update_prev(&mut state.g2_prev_sb, frame.g2_dq);
1659    update_prev(&mut state.g3_prev_sb, frame.g3_dq);
1660    update_prev(&mut state.g4_prev_sb, frame.g4_dq);
1661
1662    AcplMchOutput { z0, z2, z4, z1, z3 }
1663}
1664
1665// =====================================================================
1666// §5.7.7.6.1 Pseudocode 117 — ASPX_ACPL_1 / ASPX_ACPL_2 multichannel
1667// =====================================================================
1668
1669/// Persistent state for the §5.7.7.6.1 ASPX_ACPL_1 / ASPX_ACPL_2
1670/// multichannel wrapper (Pseudocode 117): two parallel `ACplModule`s,
1671/// each with its own `AcplCpeState` (D0 for the L-side ACplModule,
1672/// D1 for the R-side). Carried across AC-4 frames by the decoder.
1673#[derive(Debug, Clone)]
1674pub struct Acpl5xPairState {
1675    /// State for the first ACplModule — drives the L / Ls output pair
1676    /// from the L (and optionally Ls) carrier. Decorrelator = D0.
1677    pub left_pair: AcplCpeState,
1678    /// State for the second ACplModule — drives the R / Rs output pair
1679    /// from the R (and optionally Rs) carrier. Decorrelator = D1.
1680    pub right_pair: AcplCpeState,
1681    /// Differential-decode state for the first `acpl_data_1ch()` set
1682    /// (alpha / beta on the L-side module).
1683    pub alpha1_diff: AcplDiffState,
1684    pub beta1_diff: AcplDiffState,
1685    /// Differential-decode state for the second `acpl_data_1ch()` set
1686    /// (alpha / beta on the R-side module).
1687    pub alpha2_diff: AcplDiffState,
1688    pub beta2_diff: AcplDiffState,
1689}
1690
1691impl Acpl5xPairState {
1692    pub fn new() -> Self {
1693        Self {
1694            left_pair: AcplCpeState::new(DecorrelatorId::D0),
1695            right_pair: AcplCpeState::new(DecorrelatorId::D1),
1696            alpha1_diff: AcplDiffState::new(),
1697            beta1_diff: AcplDiffState::new(),
1698            alpha2_diff: AcplDiffState::new(),
1699            beta2_diff: AcplDiffState::new(),
1700        }
1701    }
1702}
1703
1704impl Default for Acpl5xPairState {
1705    fn default() -> Self {
1706        Self::new()
1707    }
1708}
1709
1710/// 5_X codec mode selector for [`run_pseudocode_117_5x`] — chooses
1711/// between the two-carrier `ASPX_ACPL_1` (`x3`/`x4` present) and the
1712/// one-carrier `ASPX_ACPL_2` (`x3`/`x4` are zero / absent).
1713#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1714pub enum Acpl5xPairMode {
1715    /// `5_X_codec_mode == ASPX_ACPL_1` — L/R carriers + Ls/Rs carriers
1716    /// (4 driving channels into the two parallel ACplModule's).
1717    AspxAcpl1,
1718    /// `5_X_codec_mode == ASPX_ACPL_2` — only L/R carriers; Ls/Rs come
1719    /// from each ACplModule's decorrelator-driven `y0`/`y1` term.
1720    AspxAcpl2,
1721}
1722
1723/// Inputs to one §5.7.7.6.1 Pseudocode 117 wrapper pass.
1724///
1725/// Carrier channel ordering follows the spec:
1726///
1727/// * `x0` = Left, `x1` = Right (always present)
1728/// * `x2` = Centre (passthrough — copied straight to `z4`)
1729/// * `x3` = Left surround, `x4` = Right surround (only used when
1730///   `mode == AspxAcpl1`; `None` for `AspxAcpl2`).
1731///
1732/// Each ACplModule consumes its own `acpl_data_1ch()` set (the
1733/// dequantised `acpl_alpha_1_dq` / `acpl_beta_1_dq` for module #1, and
1734/// `acpl_alpha_2_dq` / `acpl_beta_2_dq` for module #2). `num_pset_1` /
1735/// `num_pset_2` and the two framing rows can differ between modules.
1736pub struct Acpl5xPairFrame<'a> {
1737    pub mode: Acpl5xPairMode,
1738    pub x0: &'a [[(f32, f32); NUM_QMF_SUBBANDS]],
1739    pub x1: &'a [[(f32, f32); NUM_QMF_SUBBANDS]],
1740    pub x2: &'a [[(f32, f32); NUM_QMF_SUBBANDS]],
1741    pub x3: Option<&'a [[(f32, f32); NUM_QMF_SUBBANDS]]>,
1742    pub x4: Option<&'a [[(f32, f32); NUM_QMF_SUBBANDS]]>,
1743    /// `acpl_alpha_1_dq[pset][pb]` / `acpl_beta_1_dq[pset][pb]` (module
1744    /// #1, drives L / Ls).
1745    pub alpha_1_dq: &'a [Vec<f32>],
1746    pub beta_1_dq: &'a [Vec<f32>],
1747    /// `acpl_alpha_2_dq[pset][pb]` / `acpl_beta_2_dq[pset][pb]` (module
1748    /// #2, drives R / Rs).
1749    pub alpha_2_dq: &'a [Vec<f32>],
1750    pub beta_2_dq: &'a [Vec<f32>],
1751    pub num_param_bands: u32,
1752    /// The multichannel paths set `acpl_qmf_band` to 0 (see
1753    /// §5.7.7.6.1) — the M/S split below `acpl_qmf_band` only applies
1754    /// to the channel-pair flow.
1755    pub acpl_qmf_band: u32,
1756    pub steep_1: bool,
1757    pub steep_2: bool,
1758    pub param_timeslots_1: &'a [u8],
1759    pub param_timeslots_2: &'a [u8],
1760}
1761
1762/// Output channels from [`run_pseudocode_117_5x`] — the five 5.X output
1763/// QMF matrices in spec ordering (`z0`=L, `z1`=Ls, `z2`=R, `z3`=Rs,
1764/// `z4`=C).
1765pub struct Acpl5xPairOutput {
1766    pub z0: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>, // L
1767    pub z1: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>, // Ls
1768    pub z2: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>, // R
1769    pub z3: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>, // Rs
1770    pub z4: Vec<[(f32, f32); NUM_QMF_SUBBANDS]>, // C
1771}
1772
1773/// Run the §5.7.7.6.1 Pseudocode 117 multichannel wrapper for
1774/// `5_X_codec_mode in { ASPX_ACPL_1, ASPX_ACPL_2 }`.
1775///
1776/// Verbatim from Pseudocode 117 (TS 103 190-1 §5.7.7.6.1):
1777///
1778/// ```text
1779///   x0in = 2*x0;
1780///   x1in = 2*x1;
1781///   u0 = inputSignalModification(x0in);   // D0
1782///   u1 = inputSignalModification(x1in);   // D1
1783///   y0 = applyTransientDucker(u0);
1784///   y1 = applyTransientDucker(u1);
1785///   if (5_X_codec_mode == ASPX_ACPL_1) {
1786///       x3in = 2*x3;
1787///       x4in = 2*x4;
1788///       (z0, z1) = ACplModule(alpha_1, beta_1, num_pset_1, x0in, x3in, y0);
1789///       (z2, z3) = ACplModule(alpha_2, beta_2, num_pset_2, x1in, x4in, y1);
1790///   }
1791///   else if (5_X_codec_mode == ASPX_ACPL_2) {
1792///       (z0, z1) = ACplModule(alpha_1, beta_1, num_pset_1, x0in, 0, y0);
1793///       (z2, z3) = ACplModule(alpha_2, beta_2, num_pset_2, x1in, 0, y1);
1794///   }
1795///   z1 *= sqrt(2);
1796///   z3 *= sqrt(2);
1797///   z4 = x2;
1798/// ```
1799///
1800/// Each `ACplModule` is invoked through [`run_pseudocode_115_pair`] so
1801/// the L-side and R-side decorrelator (D0/D1) + ducker state stays
1802/// chained across frames in `state.left_pair` / `state.right_pair`.
1803pub fn run_pseudocode_117_5x(
1804    state: &mut Acpl5xPairState,
1805    frame: Acpl5xPairFrame<'_>,
1806) -> Acpl5xPairOutput {
1807    let num_ts = frame.x0.len();
1808    debug_assert_eq!(frame.x1.len(), num_ts);
1809    debug_assert_eq!(frame.x2.len(), num_ts);
1810
1811    // Module #1: L-side. ASPX_ACPL_1 -> x1 = x3 (Ls carrier).
1812    //                   ASPX_ACPL_2 -> x1 = None.
1813    let pair1_x1 = match frame.mode {
1814        Acpl5xPairMode::AspxAcpl1 => frame.x3,
1815        Acpl5xPairMode::AspxAcpl2 => None,
1816    };
1817    let pair1 = AcplCpeFrame {
1818        x0: frame.x0,
1819        x1: pair1_x1,
1820        alpha_dq: frame.alpha_1_dq,
1821        beta_dq: frame.beta_1_dq,
1822        num_param_bands: frame.num_param_bands,
1823        acpl_qmf_band: frame.acpl_qmf_band,
1824        steep: frame.steep_1,
1825        param_timeslots: frame.param_timeslots_1,
1826    };
1827    let out1 = run_pseudocode_115_pair(&mut state.left_pair, pair1);
1828
1829    // Module #2: R-side. ASPX_ACPL_1 -> x1 = x4 (Rs carrier).
1830    //                   ASPX_ACPL_2 -> x1 = None.
1831    let pair2_x1 = match frame.mode {
1832        Acpl5xPairMode::AspxAcpl1 => frame.x4,
1833        Acpl5xPairMode::AspxAcpl2 => None,
1834    };
1835    let pair2 = AcplCpeFrame {
1836        x0: frame.x1,
1837        x1: pair2_x1,
1838        alpha_dq: frame.alpha_2_dq,
1839        beta_dq: frame.beta_2_dq,
1840        num_param_bands: frame.num_param_bands,
1841        acpl_qmf_band: frame.acpl_qmf_band,
1842        steep: frame.steep_2,
1843        param_timeslots: frame.param_timeslots_2,
1844    };
1845    let out2 = run_pseudocode_115_pair(&mut state.right_pair, pair2);
1846
1847    // Step "z1 *= sqrt(2)", "z3 *= sqrt(2)" — the surround pair gets the
1848    // sqrt(2) scale per the spec.
1849    let sq2 = (2.0f32).sqrt();
1850    let scale_inplace = |z: &mut [[(f32, f32); NUM_QMF_SUBBANDS]], s: f32| {
1851        for col in z.iter_mut() {
1852            for sb in 0..NUM_QMF_SUBBANDS {
1853                col[sb].0 *= s;
1854                col[sb].1 *= s;
1855            }
1856        }
1857    };
1858    let mut z0 = out1.z0;
1859    let mut z1 = out1.z1;
1860    let mut z2 = out2.z0;
1861    let mut z3 = out2.z1;
1862    scale_inplace(&mut z1, sq2);
1863    scale_inplace(&mut z3, sq2);
1864
1865    // Centre channel passthrough: z4 = x2 (no scale per the pseudocode).
1866    let z4: Vec<[(f32, f32); NUM_QMF_SUBBANDS]> = frame.x2.to_vec();
1867
1868    // Borrow-checker quietener: z0/z2 are intentionally not re-scaled.
1869    let _ = (&mut z0, &mut z2);
1870
1871    Acpl5xPairOutput { z0, z1, z2, z3, z4 }
1872}
1873
1874// =====================================================================
1875// §5.7.7 — top-level helpers wired into Ac4Decoder
1876// =====================================================================
1877
1878use crate::acpl::{AcplConfig1ch, AcplData1ch, AcplInterpolationType};
1879use crate::qmf::{QmfAnalysisBank, QmfSynthesisBank};
1880
1881/// Per-substream A-CPL persistent state — diff state for ALPHA and BETA
1882/// plus the channel-pair `AcplCpeState` (decorrelator + ducker + prev
1883/// arrays). Carried across AC-4 frames by the decoder.
1884#[derive(Debug, Clone)]
1885pub struct AcplSubstreamState {
1886    pub alpha_diff: AcplDiffState,
1887    pub beta_diff: AcplDiffState,
1888    pub cpe: AcplCpeState,
1889}
1890
1891impl AcplSubstreamState {
1892    pub fn new() -> Self {
1893        Self {
1894            alpha_diff: AcplDiffState::new(),
1895            beta_diff: AcplDiffState::new(),
1896            cpe: AcplCpeState::new(DecorrelatorId::D0),
1897        }
1898    }
1899}
1900
1901impl Default for AcplSubstreamState {
1902    fn default() -> Self {
1903        Self::new()
1904    }
1905}
1906
1907/// Run the §5.7.7 A-CPL channel-pair synthesis on a mono PCM input
1908/// (already ASPX-extended) and emit stereo PCM via QMF
1909/// analysis → A-CPL → QMF synthesis.
1910///
1911/// Spec wiring (ETSI TS 103 190-1):
1912///   * `pcm_in` — mono ASPX-extended PCM, length must be a multiple of
1913///     64 (one QMF slot = 64 samples).
1914///   * `cfg` — parsed `acpl_config_1ch()` (PARTIAL or FULL) for the
1915///     active substream (§4.2.13.1 Table 59).
1916///   * `data` — parsed `acpl_data_1ch()` (§4.2.13.3 Table 61).
1917///   * `state` — per-substream state carried across frames.
1918///
1919/// Returns `(left, right)` interleaved-friendly PCM buffers, each the
1920/// same length as `pcm_in`, or `None` if the input length isn't aligned
1921/// to a QMF slot boundary or the parameters are inconsistent.
1922pub fn run_acpl_1ch_pcm(
1923    pcm_in: &[f32],
1924    cfg: &AcplConfig1ch,
1925    data: &AcplData1ch,
1926    state: &mut AcplSubstreamState,
1927) -> Option<(Vec<f32>, Vec<f32>)> {
1928    if pcm_in.is_empty() || pcm_in.len() % NUM_QMF_SUBBANDS != 0 {
1929        return None;
1930    }
1931    let n_slots = pcm_in.len() / NUM_QMF_SUBBANDS;
1932    if n_slots == 0 {
1933        return None;
1934    }
1935    // Forward QMF analysis on the input PCM. `process_block` already
1936    // returns the per-slot `[(re, im); 64]` columns we need.
1937    let mut ana = QmfAnalysisBank::new();
1938    let x0 = ana.process_block(pcm_in);
1939    // Differential decode + dequantize ALPHA / BETA.
1940    let alpha_q = differential_decode(&data.alpha1, cfg.num_param_bands, &mut state.alpha_diff);
1941    let beta_q = differential_decode(&data.beta1, cfg.num_param_bands, &mut state.beta_diff);
1942    let (alpha_dq, beta_dq) = dequantize_alpha_beta(&alpha_q, &beta_q, cfg.quant_mode);
1943    if alpha_dq.is_empty() {
1944        return None;
1945    }
1946    // Run the §5.7.7.5 channel-pair element. ASPX_ACPL_2 has x1 == None
1947    // (single ASPX channel — the spec's "1-channel A-CPL").
1948    let frame = AcplCpeFrame {
1949        x0: &x0,
1950        x1: None,
1951        alpha_dq: &alpha_dq,
1952        beta_dq: &beta_dq,
1953        num_param_bands: cfg.num_param_bands,
1954        acpl_qmf_band: cfg.qmf_band as u32,
1955        steep: matches!(
1956            data.framing.interpolation_type,
1957            AcplInterpolationType::Steep
1958        ),
1959        param_timeslots: &data.framing.param_timeslots,
1960    };
1961    let out = run_pseudocode_115_pair(&mut state.cpe, frame);
1962    // Inverse QMF synthesis for both output channels.
1963    let mut syn0 = QmfSynthesisBank::new();
1964    let mut syn1 = QmfSynthesisBank::new();
1965    let mut left = Vec::with_capacity(pcm_in.len());
1966    let mut right = Vec::with_capacity(pcm_in.len());
1967    for ts in 0..n_slots {
1968        left.extend_from_slice(&syn0.process_slot(&out.z0[ts]));
1969        right.extend_from_slice(&syn1.process_slot(&out.z1[ts]));
1970    }
1971    Some((left, right))
1972}
1973
1974/// Run the §5.7.7 A-CPL channel-pair synthesis on a stereo PCM input
1975/// (M / S channels from a joint-MDCT body, both already ASPX-extended on
1976/// the M side per `aspx_data_1ch()`) and emit stereo PCM via QMF
1977/// analysis → A-CPL → QMF synthesis.
1978///
1979/// Spec wiring (ETSI TS 103 190-1 §4.2.6.3 ASPX_ACPL_1):
1980///   * `pcm_m` / `pcm_s` — joint-MDCT M (mid) and S (side) PCM streams,
1981///     same length, multiple of 64 samples.
1982///   * `cfg` — parsed `acpl_config_1ch(PARTIAL)` for the active substream.
1983///   * `data` — parsed `acpl_data_1ch()` (§4.2.13.3 Table 61).
1984///   * `state` — per-substream state carried across frames.
1985///
1986/// In the spec's `acpl_module()` (Pseudocode 116) the low subbands below
1987/// `acpl_qmf_band` recover `(L, R)` via the inverse-M/S split
1988/// `z0 = 0.5*(x0+x1)`, `z1 = 0.5*(x0-x1)`; the upper subbands re-mix the
1989/// decorrelator output `y` (computed from `x0`) into the second channel
1990/// using the parametric `alpha`/`beta` coefficients. We feed `pcm_m` into
1991/// `x0` and `pcm_s` into `x1` here, matching the spec's M/S convention.
1992pub fn run_acpl_1ch_pcm_stereo(
1993    pcm_m: &[f32],
1994    pcm_s: &[f32],
1995    cfg: &AcplConfig1ch,
1996    data: &AcplData1ch,
1997    state: &mut AcplSubstreamState,
1998) -> Option<(Vec<f32>, Vec<f32>)> {
1999    if pcm_m.is_empty() || pcm_m.len() % NUM_QMF_SUBBANDS != 0 || pcm_m.len() != pcm_s.len() {
2000        return None;
2001    }
2002    let n_slots = pcm_m.len() / NUM_QMF_SUBBANDS;
2003    if n_slots == 0 {
2004        return None;
2005    }
2006    let mut ana_m = QmfAnalysisBank::new();
2007    let mut ana_s = QmfAnalysisBank::new();
2008    let x0 = ana_m.process_block(pcm_m);
2009    let x1 = ana_s.process_block(pcm_s);
2010    let alpha_q = differential_decode(&data.alpha1, cfg.num_param_bands, &mut state.alpha_diff);
2011    let beta_q = differential_decode(&data.beta1, cfg.num_param_bands, &mut state.beta_diff);
2012    let (alpha_dq, beta_dq) = dequantize_alpha_beta(&alpha_q, &beta_q, cfg.quant_mode);
2013    if alpha_dq.is_empty() {
2014        return None;
2015    }
2016    let frame = AcplCpeFrame {
2017        x0: &x0,
2018        x1: Some(&x1),
2019        alpha_dq: &alpha_dq,
2020        beta_dq: &beta_dq,
2021        num_param_bands: cfg.num_param_bands,
2022        acpl_qmf_band: cfg.qmf_band as u32,
2023        steep: matches!(
2024            data.framing.interpolation_type,
2025            AcplInterpolationType::Steep
2026        ),
2027        param_timeslots: &data.framing.param_timeslots,
2028    };
2029    let out = run_pseudocode_115_pair(&mut state.cpe, frame);
2030    let mut syn0 = QmfSynthesisBank::new();
2031    let mut syn1 = QmfSynthesisBank::new();
2032    let mut left = Vec::with_capacity(pcm_m.len());
2033    let mut right = Vec::with_capacity(pcm_m.len());
2034    for ts in 0..n_slots {
2035        left.extend_from_slice(&syn0.process_slot(&out.z0[ts]));
2036        right.extend_from_slice(&syn1.process_slot(&out.z1[ts]));
2037    }
2038    Some((left, right))
2039}
2040
2041// =====================================================================
2042// §5.7.7.6 — top-level helpers wired into the 5_X channel-element
2043// =====================================================================
2044
2045use crate::acpl::{AcplConfig2ch, AcplData1ch as AcplData1chTy, AcplData2ch};
2046
2047/// Five-channel PCM output bundle from the 5_X A-CPL pipelines —
2048/// (L, R, C, Ls, Rs) in spec ordering.
2049#[derive(Debug, Clone)]
2050pub struct Acpl5xPcmOutput {
2051    pub left: Vec<f32>,
2052    pub right: Vec<f32>,
2053    pub centre: Vec<f32>,
2054    pub left_surround: Vec<f32>,
2055    pub right_surround: Vec<f32>,
2056}
2057
2058/// Per-substream A-CPL state for the 5_X `ASPX_ACPL_1` /
2059/// `ASPX_ACPL_2` pair pipeline (Pseudocode 117). Composed of the
2060/// [`Acpl5xPairState`] plus the L/R QMF analysis + per-output QMF
2061/// synthesis banks needed to round-trip PCM end-to-end.
2062pub struct Acpl5xPairPcmState {
2063    pub pair: Acpl5xPairState,
2064    pub ana_l: QmfAnalysisBank,
2065    pub ana_r: QmfAnalysisBank,
2066    pub ana_c: QmfAnalysisBank,
2067    pub ana_ls: QmfAnalysisBank,
2068    pub ana_rs: QmfAnalysisBank,
2069    pub syn_l: QmfSynthesisBank,
2070    pub syn_r: QmfSynthesisBank,
2071    pub syn_c: QmfSynthesisBank,
2072    pub syn_ls: QmfSynthesisBank,
2073    pub syn_rs: QmfSynthesisBank,
2074}
2075
2076impl Acpl5xPairPcmState {
2077    pub fn new() -> Self {
2078        Self {
2079            pair: Acpl5xPairState::new(),
2080            ana_l: QmfAnalysisBank::new(),
2081            ana_r: QmfAnalysisBank::new(),
2082            ana_c: QmfAnalysisBank::new(),
2083            ana_ls: QmfAnalysisBank::new(),
2084            ana_rs: QmfAnalysisBank::new(),
2085            syn_l: QmfSynthesisBank::new(),
2086            syn_r: QmfSynthesisBank::new(),
2087            syn_c: QmfSynthesisBank::new(),
2088            syn_ls: QmfSynthesisBank::new(),
2089            syn_rs: QmfSynthesisBank::new(),
2090        }
2091    }
2092}
2093
2094impl Default for Acpl5xPairPcmState {
2095    fn default() -> Self {
2096        Self::new()
2097    }
2098}
2099
2100/// Run the §5.7.7.6.1 Pseudocode 117 ASPX_ACPL_1 / ASPX_ACPL_2 5_X
2101/// pipeline end-to-end at PCM granularity.
2102///
2103/// Inputs (every PCM buffer must be the same length and a multiple of
2104/// 64 samples — one QMF slot per 64 samples):
2105///
2106/// * `pcm_l` / `pcm_r` — already-ASPX-extended carrier PCM for the
2107///   primary pair. Required.
2108/// * `pcm_c` — centre channel PCM (passthrough → `centre` output).
2109/// * `pcm_ls` / `pcm_rs` — surround carriers for ASPX_ACPL_1 (`mode ==
2110///   AspxAcpl1`); `None` for ASPX_ACPL_2.
2111/// * `cfg_1` / `data_1` — `acpl_config_1ch()` + `acpl_data_1ch()` for
2112///   the L-side ACplModule (alpha_1 / beta_1).
2113/// * `cfg_2` / `data_2` — same, for the R-side ACplModule (alpha_2 /
2114///   beta_2).
2115/// * `state` — per-substream state (carries decorrelator + ducker IIR
2116///   state and the ALPHA / BETA differential-decode rolling sums
2117///   across frames).
2118///
2119/// Returns the five-channel PCM bundle, or `None` if the input shape
2120/// is invalid (mismatched length / not a multiple of 64 / inconsistent
2121/// surround presence vs. mode).
2122#[allow(clippy::too_many_arguments)]
2123pub fn run_acpl_5x_pair_pcm(
2124    mode: Acpl5xPairMode,
2125    pcm_l: &[f32],
2126    pcm_r: &[f32],
2127    pcm_c: &[f32],
2128    pcm_ls: Option<&[f32]>,
2129    pcm_rs: Option<&[f32]>,
2130    cfg_1: &AcplConfig1ch,
2131    data_1: &AcplData1chTy,
2132    cfg_2: &AcplConfig1ch,
2133    data_2: &AcplData1chTy,
2134    state: &mut Acpl5xPairPcmState,
2135) -> Option<Acpl5xPcmOutput> {
2136    if pcm_l.is_empty() || pcm_l.len() % NUM_QMF_SUBBANDS != 0 {
2137        return None;
2138    }
2139    if pcm_l.len() != pcm_r.len() || pcm_l.len() != pcm_c.len() {
2140        return None;
2141    }
2142    let n_slots = pcm_l.len() / NUM_QMF_SUBBANDS;
2143    if n_slots == 0 {
2144        return None;
2145    }
2146    // Mode → surround carrier presence consistency check.
2147    match mode {
2148        Acpl5xPairMode::AspxAcpl1 => {
2149            let (ls, rs) = (pcm_ls?, pcm_rs?);
2150            if ls.len() != pcm_l.len() || rs.len() != pcm_l.len() {
2151                return None;
2152            }
2153        }
2154        Acpl5xPairMode::AspxAcpl2 => {
2155            // Surround carriers must be absent in ASPX_ACPL_2 mode.
2156            if pcm_ls.is_some() || pcm_rs.is_some() {
2157                return None;
2158            }
2159        }
2160    }
2161    // QMF analysis on every active input.
2162    let x0 = state.ana_l.process_block(pcm_l);
2163    let x1 = state.ana_r.process_block(pcm_r);
2164    let x2 = state.ana_c.process_block(pcm_c);
2165    let x3_owned = pcm_ls.map(|p| state.ana_ls.process_block(p));
2166    let x4_owned = pcm_rs.map(|p| state.ana_rs.process_block(p));
2167
2168    // Differential-decode the two acpl_data_1ch parameter sets.
2169    let alpha1_q = differential_decode(
2170        &data_1.alpha1,
2171        cfg_1.num_param_bands,
2172        &mut state.pair.alpha1_diff,
2173    );
2174    let beta1_q = differential_decode(
2175        &data_1.beta1,
2176        cfg_1.num_param_bands,
2177        &mut state.pair.beta1_diff,
2178    );
2179    let (alpha1_dq, beta1_dq) = dequantize_alpha_beta(&alpha1_q, &beta1_q, cfg_1.quant_mode);
2180    let alpha2_q = differential_decode(
2181        &data_2.alpha1,
2182        cfg_2.num_param_bands,
2183        &mut state.pair.alpha2_diff,
2184    );
2185    let beta2_q = differential_decode(
2186        &data_2.beta1,
2187        cfg_2.num_param_bands,
2188        &mut state.pair.beta2_diff,
2189    );
2190    let (alpha2_dq, beta2_dq) = dequantize_alpha_beta(&alpha2_q, &beta2_q, cfg_2.quant_mode);
2191    if alpha1_dq.is_empty() || alpha2_dq.is_empty() {
2192        return None;
2193    }
2194    // Pseudocode 117 wrapper over Pseudocode 115 / 116.
2195    let frame = Acpl5xPairFrame {
2196        mode,
2197        x0: &x0,
2198        x1: &x1,
2199        x2: &x2,
2200        x3: x3_owned.as_deref(),
2201        x4: x4_owned.as_deref(),
2202        alpha_1_dq: &alpha1_dq,
2203        beta_1_dq: &beta1_dq,
2204        alpha_2_dq: &alpha2_dq,
2205        beta_2_dq: &beta2_dq,
2206        num_param_bands: cfg_1.num_param_bands,
2207        // 5.X multichannel paths set acpl_qmf_band = 0 per §5.7.7.6.1.
2208        acpl_qmf_band: 0,
2209        steep_1: matches!(
2210            data_1.framing.interpolation_type,
2211            AcplInterpolationType::Steep
2212        ),
2213        steep_2: matches!(
2214            data_2.framing.interpolation_type,
2215            AcplInterpolationType::Steep
2216        ),
2217        param_timeslots_1: &data_1.framing.param_timeslots,
2218        param_timeslots_2: &data_2.framing.param_timeslots,
2219    };
2220    let out = run_pseudocode_117_5x(&mut state.pair, frame);
2221
2222    // QMF synthesis on each output channel.
2223    let mut left = Vec::with_capacity(pcm_l.len());
2224    let mut right = Vec::with_capacity(pcm_l.len());
2225    let mut centre = Vec::with_capacity(pcm_l.len());
2226    let mut left_surround = Vec::with_capacity(pcm_l.len());
2227    let mut right_surround = Vec::with_capacity(pcm_l.len());
2228    for ts in 0..n_slots {
2229        left.extend_from_slice(&state.syn_l.process_slot(&out.z0[ts]));
2230        right.extend_from_slice(&state.syn_r.process_slot(&out.z2[ts]));
2231        centre.extend_from_slice(&state.syn_c.process_slot(&out.z4[ts]));
2232        left_surround.extend_from_slice(&state.syn_ls.process_slot(&out.z1[ts]));
2233        right_surround.extend_from_slice(&state.syn_rs.process_slot(&out.z3[ts]));
2234    }
2235    Some(Acpl5xPcmOutput {
2236        left,
2237        right,
2238        centre,
2239        left_surround,
2240        right_surround,
2241    })
2242}
2243
2244/// Per-substream A-CPL state for the 5_X `ASPX_ACPL_3` multichannel
2245/// pipeline (Pseudocode 118). Bundles the [`AcplMchState`] (D0/D1/D2 +
2246/// 3x ducker + per-pset prev gammas) with the L/R QMF analysis +
2247/// per-output QMF synthesis banks plus the alpha/beta/beta3/gamma
2248/// differential-decode rolling state.
2249pub struct Acpl5xMchPcmState {
2250    pub mch: AcplMchState,
2251    pub ana_l: QmfAnalysisBank,
2252    pub ana_r: QmfAnalysisBank,
2253    pub ana_c: QmfAnalysisBank,
2254    pub syn_l: QmfSynthesisBank,
2255    pub syn_r: QmfSynthesisBank,
2256    pub syn_c: QmfSynthesisBank,
2257    pub syn_ls: QmfSynthesisBank,
2258    pub syn_rs: QmfSynthesisBank,
2259    pub alpha1_diff: AcplDiffState,
2260    pub alpha2_diff: AcplDiffState,
2261    pub beta1_diff: AcplDiffState,
2262    pub beta2_diff: AcplDiffState,
2263    pub beta3_diff: AcplDiffState,
2264    pub gamma_diff: [AcplDiffState; 6],
2265}
2266
2267impl Acpl5xMchPcmState {
2268    pub fn new() -> Self {
2269        Self {
2270            mch: AcplMchState::new(),
2271            ana_l: QmfAnalysisBank::new(),
2272            ana_r: QmfAnalysisBank::new(),
2273            ana_c: QmfAnalysisBank::new(),
2274            syn_l: QmfSynthesisBank::new(),
2275            syn_r: QmfSynthesisBank::new(),
2276            syn_c: QmfSynthesisBank::new(),
2277            syn_ls: QmfSynthesisBank::new(),
2278            syn_rs: QmfSynthesisBank::new(),
2279            alpha1_diff: AcplDiffState::new(),
2280            alpha2_diff: AcplDiffState::new(),
2281            beta1_diff: AcplDiffState::new(),
2282            beta2_diff: AcplDiffState::new(),
2283            beta3_diff: AcplDiffState::new(),
2284            gamma_diff: [
2285                AcplDiffState::new(),
2286                AcplDiffState::new(),
2287                AcplDiffState::new(),
2288                AcplDiffState::new(),
2289                AcplDiffState::new(),
2290                AcplDiffState::new(),
2291            ],
2292        }
2293    }
2294}
2295
2296impl Default for Acpl5xMchPcmState {
2297    fn default() -> Self {
2298        Self::new()
2299    }
2300}
2301
2302/// Run the §5.7.7.6.2 Pseudocode 118 ASPX_ACPL_3 5_X pipeline
2303/// end-to-end at PCM granularity.
2304///
2305/// `pcm_l` / `pcm_r` are the two A-CPL carrier streams (already
2306/// ASPX-extended); `pcm_c` is the centre channel passthrough. All
2307/// three buffers must share the same length, and that length must be a
2308/// multiple of 64 samples.
2309///
2310/// `cfg` is the parsed `acpl_config_2ch()` (§4.2.13.2 Table 60); `data`
2311/// is the parsed `acpl_data_2ch()` (§4.2.13.4 Table 62) which carries
2312/// (alpha1, alpha2, beta1, beta2, beta3, gamma1..gamma6) Huffman
2313/// parameter sets. `state` is the per-substream ACPL state (carries
2314/// the D0/D1/D2 + ducker state and the differential-decode rolling
2315/// sums across frames).
2316pub fn run_acpl_5x_mch_pcm(
2317    pcm_l: &[f32],
2318    pcm_r: &[f32],
2319    pcm_c: &[f32],
2320    cfg: &AcplConfig2ch,
2321    data: &AcplData2ch,
2322    state: &mut Acpl5xMchPcmState,
2323) -> Option<Acpl5xPcmOutput> {
2324    if pcm_l.is_empty() || pcm_l.len() % NUM_QMF_SUBBANDS != 0 {
2325        return None;
2326    }
2327    if pcm_l.len() != pcm_r.len() || pcm_l.len() != pcm_c.len() {
2328        return None;
2329    }
2330    let n_slots = pcm_l.len() / NUM_QMF_SUBBANDS;
2331    if n_slots == 0 {
2332        return None;
2333    }
2334    // QMF analysis on the two carriers + the centre passthrough.
2335    let x0 = state.ana_l.process_block(pcm_l);
2336    let x1 = state.ana_r.process_block(pcm_r);
2337    let x2 = state.ana_c.process_block(pcm_c);
2338    // Differential-decode all 11 parameter sets.
2339    let nb = cfg.num_param_bands;
2340    let alpha1_q = differential_decode(&data.alpha1, nb, &mut state.alpha1_diff);
2341    let alpha2_q = differential_decode(&data.alpha2, nb, &mut state.alpha2_diff);
2342    let beta1_q = differential_decode(&data.beta1, nb, &mut state.beta1_diff);
2343    let beta2_q = differential_decode(&data.beta2, nb, &mut state.beta2_diff);
2344    let beta3_q = differential_decode(&data.beta3, nb, &mut state.beta3_diff);
2345    let g1q = differential_decode(&data.gamma1, nb, &mut state.gamma_diff[0]);
2346    let g2q = differential_decode(&data.gamma2, nb, &mut state.gamma_diff[1]);
2347    let g3q = differential_decode(&data.gamma3, nb, &mut state.gamma_diff[2]);
2348    let g4q = differential_decode(&data.gamma4, nb, &mut state.gamma_diff[3]);
2349    let g5q = differential_decode(&data.gamma5, nb, &mut state.gamma_diff[4]);
2350    let g6q = differential_decode(&data.gamma6, nb, &mut state.gamma_diff[5]);
2351    let (alpha1_dq, beta1_dq) = dequantize_alpha_beta(&alpha1_q, &beta1_q, cfg.quant_mode_0);
2352    let (alpha2_dq, beta2_dq) = dequantize_alpha_beta(&alpha2_q, &beta2_q, cfg.quant_mode_0);
2353    let beta3_dq = dequantize_beta3(&beta3_q, cfg.quant_mode_0);
2354    let g1_dq = dequantize_gamma(&g1q, cfg.quant_mode_1);
2355    let g2_dq = dequantize_gamma(&g2q, cfg.quant_mode_1);
2356    let g3_dq = dequantize_gamma(&g3q, cfg.quant_mode_1);
2357    let g4_dq = dequantize_gamma(&g4q, cfg.quant_mode_1);
2358    let g5_dq = dequantize_gamma(&g5q, cfg.quant_mode_1);
2359    let g6_dq = dequantize_gamma(&g6q, cfg.quant_mode_1);
2360    if alpha1_dq.is_empty() {
2361        return None;
2362    }
2363    let frame = AcplMchFrame {
2364        x0: &x0,
2365        x1: &x1,
2366        x2: &x2,
2367        alpha_1_dq: &alpha1_dq,
2368        alpha_2_dq: &alpha2_dq,
2369        beta_1_dq: &beta1_dq,
2370        beta_2_dq: &beta2_dq,
2371        beta_3_dq: &beta3_dq,
2372        g1_dq: &g1_dq,
2373        g2_dq: &g2_dq,
2374        g3_dq: &g3_dq,
2375        g4_dq: &g4_dq,
2376        g5_dq: &g5_dq,
2377        g6_dq: &g6_dq,
2378        num_param_bands: nb,
2379        steep: matches!(
2380            data.framing.interpolation_type,
2381            AcplInterpolationType::Steep
2382        ),
2383        param_timeslots: &data.framing.param_timeslots,
2384    };
2385    let out = run_pseudocode_118_5x(&mut state.mch, frame);
2386    let mut left = Vec::with_capacity(pcm_l.len());
2387    let mut right = Vec::with_capacity(pcm_l.len());
2388    let mut centre = Vec::with_capacity(pcm_l.len());
2389    let mut left_surround = Vec::with_capacity(pcm_l.len());
2390    let mut right_surround = Vec::with_capacity(pcm_l.len());
2391    for ts in 0..n_slots {
2392        left.extend_from_slice(&state.syn_l.process_slot(&out.z0[ts]));
2393        right.extend_from_slice(&state.syn_r.process_slot(&out.z2[ts]));
2394        centre.extend_from_slice(&state.syn_c.process_slot(&out.z4[ts]));
2395        left_surround.extend_from_slice(&state.syn_ls.process_slot(&out.z1[ts]));
2396        right_surround.extend_from_slice(&state.syn_rs.process_slot(&out.z3[ts]));
2397    }
2398    Some(Acpl5xPcmOutput {
2399        left,
2400        right,
2401        centre,
2402        left_surround,
2403        right_surround,
2404    })
2405}
2406
2407// =====================================================================
2408// Tests
2409// =====================================================================
2410
2411#[cfg(test)]
2412mod tests {
2413    use super::*;
2414    use crate::acpl::AcplHuffParam;
2415
2416    // ---------------- §5.7.7.7 dequantisation tables -----------------
2417
2418    #[test]
2419    fn alpha_dq_fine_anchors_table_203() {
2420        // Table 203 anchors: index 0 = -2.0, index 16 = 0.0, index 32 = +2.0
2421        assert_eq!(ALPHA_DQ_FINE[0], -2.000000);
2422        assert_eq!(ALPHA_DQ_FINE[16], 0.000000);
2423        assert_eq!(ALPHA_DQ_FINE[32], 2.000000);
2424        // Anti-symmetry around the centre: alpha[16+k] = -alpha[16-k].
2425        for k in 1..=16 {
2426            let lo = ALPHA_DQ_FINE[16 - k];
2427            let hi = ALPHA_DQ_FINE[16 + k];
2428            assert!((lo + hi).abs() < 1e-6, "k={k} lo={lo} hi={hi}");
2429        }
2430    }
2431
2432    #[test]
2433    fn alpha_dq_coarse_anchors_table_205() {
2434        assert_eq!(ALPHA_DQ_COARSE[0], -2.000000);
2435        assert_eq!(ALPHA_DQ_COARSE[8], 0.000000);
2436        assert_eq!(ALPHA_DQ_COARSE[16], 2.000000);
2437        for k in 1..=8 {
2438            let lo = ALPHA_DQ_COARSE[8 - k];
2439            let hi = ALPHA_DQ_COARSE[8 + k];
2440            assert!((lo + hi).abs() < 1e-6, "k={k} lo={lo} hi={hi}");
2441        }
2442    }
2443
2444    #[test]
2445    fn ibeta_fine_table_203_column2() {
2446        assert_eq!(IBETA_FINE[0], 0);
2447        assert_eq!(IBETA_FINE[8], 8);
2448        assert_eq!(IBETA_FINE[16], 0);
2449        assert_eq!(IBETA_FINE[24], 8);
2450        assert_eq!(IBETA_FINE[32], 0);
2451    }
2452
2453    #[test]
2454    fn ibeta_coarse_table_205_column2() {
2455        assert_eq!(IBETA_COARSE[0], 0);
2456        assert_eq!(IBETA_COARSE[4], 4);
2457        assert_eq!(IBETA_COARSE[8], 0);
2458        assert_eq!(IBETA_COARSE[12], 4);
2459        assert_eq!(IBETA_COARSE[16], 0);
2460    }
2461
2462    #[test]
2463    fn beta_dq_fine_anchors_table_204() {
2464        // beta_q=0 row is all zeros.
2465        for col in 0..9 {
2466            assert_eq!(BETA_DQ_FINE[0][col], 0.0);
2467        }
2468        // Anchor: beta_q=8, ibeta=0 → 4.0; ibeta=8 → 1.0.
2469        assert_eq!(BETA_DQ_FINE[8][0], 4.0);
2470        assert_eq!(BETA_DQ_FINE[8][8], 1.0);
2471        // Strict monotone-decrease across ibeta for any non-zero beta_q.
2472        for q in 1..=8 {
2473            for c in 1..9 {
2474                assert!(
2475                    BETA_DQ_FINE[q][c] < BETA_DQ_FINE[q][c - 1],
2476                    "beta_q={q} col={c}"
2477                );
2478            }
2479        }
2480    }
2481
2482    #[test]
2483    fn beta_dq_coarse_anchors_table_206() {
2484        assert_eq!(BETA_DQ_COARSE[0][0], 0.0);
2485        assert_eq!(BETA_DQ_COARSE[4][0], 4.0);
2486        assert_eq!(BETA_DQ_COARSE[4][4], 1.0);
2487        for q in 1..=4 {
2488            for c in 1..5 {
2489                assert!(
2490                    BETA_DQ_COARSE[q][c] < BETA_DQ_COARSE[q][c - 1],
2491                    "beta_q={q} col={c}"
2492                );
2493            }
2494        }
2495    }
2496
2497    #[test]
2498    fn beta3_and_gamma_deltas_match_tables_207_208() {
2499        assert_eq!(beta3_delta(AcplQuantMode::Fine), 0.125);
2500        assert_eq!(beta3_delta(AcplQuantMode::Coarse), 0.25);
2501        assert!((gamma_delta(AcplQuantMode::Fine) - 1638.0 / 16384.0).abs() < 1e-9);
2502        assert!((gamma_delta(AcplQuantMode::Coarse) - 3276.0 / 16384.0).abs() < 1e-9);
2503    }
2504
2505    #[test]
2506    fn dequantize_alpha_fine_round_trips_through_signed_index() {
2507        // alpha_q = 0 (Huffman F0 cb_off=16 → recovered i32=0) → lane
2508        // 16 → +0.0.
2509        let (a, ib) = dequantize_alpha_index(AcplQuantMode::Fine, 0);
2510        assert_eq!(a, 0.0);
2511        assert_eq!(ib, 0);
2512        // alpha_q = -16 (lane 0) → -2.0.
2513        let (a, ib) = dequantize_alpha_index(AcplQuantMode::Fine, -16);
2514        assert_eq!(a, -2.000000);
2515        assert_eq!(ib, 0);
2516        // alpha_q = +8 (lane 24) → +1.0.
2517        let (a, ib) = dequantize_alpha_index(AcplQuantMode::Fine, 8);
2518        assert_eq!(a, 1.000000);
2519        assert_eq!(ib, 8);
2520    }
2521
2522    #[test]
2523    fn dequantize_beta_fine_uses_ibeta_column() {
2524        // beta_q=+1, ibeta=0 → +0.2375.
2525        let v = dequantize_beta_index(AcplQuantMode::Fine, 1, 0);
2526        assert!((v - 0.2375).abs() < 1e-6);
2527        // beta_q=-1, ibeta=0 → -0.2375.
2528        let v = dequantize_beta_index(AcplQuantMode::Fine, -1, 0);
2529        assert!((v + 0.2375).abs() < 1e-6);
2530        // beta_q=+8, ibeta=8 → +1.0.
2531        let v = dequantize_beta_index(AcplQuantMode::Fine, 8, 8);
2532        assert!((v - 1.0).abs() < 1e-6);
2533    }
2534
2535    // ----------------- §5.7.7.7 differential decode ------------------
2536
2537    #[test]
2538    fn differential_decode_freq_accumulates_from_seed() {
2539        // DIFF_FREQ: row[0]=values[0], row[i]=row[i-1]+values[i].
2540        let p = AcplHuffParam {
2541            values: vec![5, 1, -2, 3],
2542            direction_time: false,
2543        };
2544        let mut st = AcplDiffState::new();
2545        let out = differential_decode(&[p], 4, &mut st);
2546        assert_eq!(out, vec![vec![5, 6, 4, 7]]);
2547        assert_eq!(st.q_prev, vec![5, 6, 4, 7]);
2548    }
2549
2550    #[test]
2551    fn differential_decode_time_uses_prev_then_carries() {
2552        // First set DIFF_FREQ → seed q_prev. Second set DIFF_TIME → adds
2553        // to q_prev band-by-band.
2554        let p1 = AcplHuffParam {
2555            values: vec![1, 2, 3],
2556            direction_time: false,
2557        };
2558        let p2 = AcplHuffParam {
2559            values: vec![10, -1, 0],
2560            direction_time: true,
2561        };
2562        let mut st = AcplDiffState::new();
2563        let out = differential_decode(&[p1, p2], 3, &mut st);
2564        // p1 → [1, 3, 6]
2565        // p2 (DIFF_TIME) → [1+10, 3-1, 6+0] = [11, 2, 6]
2566        assert_eq!(out, vec![vec![1, 3, 6], vec![11, 2, 6]]);
2567        assert_eq!(st.q_prev, vec![11, 2, 6]);
2568    }
2569
2570    #[test]
2571    fn differential_decode_carries_across_frames() {
2572        // Frame 1 sets q_prev = [4, 5]. Frame 2 starts with DIFF_TIME
2573        // and should pick up q_prev from frame 1.
2574        let mut st = AcplDiffState::new();
2575        let p1 = AcplHuffParam {
2576            values: vec![4, 1],
2577            direction_time: false,
2578        };
2579        let _ = differential_decode(&[p1], 2, &mut st);
2580        assert_eq!(st.q_prev, vec![4, 5]);
2581        let p2 = AcplHuffParam {
2582            values: vec![1, -2],
2583            direction_time: true,
2584        };
2585        let out2 = differential_decode(&[p2], 2, &mut st);
2586        assert_eq!(out2, vec![vec![5, 3]]);
2587        assert_eq!(st.q_prev, vec![5, 3]);
2588    }
2589
2590    // -------------- §5.7.7.7 dequantize_alpha_beta -------------------
2591
2592    #[test]
2593    fn dequantize_alpha_beta_returns_two_synced_matrices() {
2594        // 1 param set, 2 bands. alpha_q = [0, 8] (fine, lanes 16+0=16
2595        // and 16+8=24 → 0.0 and 1.0; ibetas 0 and 8). beta_q = [+1, -1]
2596        // → +0.2375 (col 0), -0.0593750 (col 8).
2597        let alpha_q = vec![vec![0i32, 8]];
2598        let beta_q = vec![vec![1i32, -1]];
2599        let (a, b) = dequantize_alpha_beta(&alpha_q, &beta_q, AcplQuantMode::Fine);
2600        assert_eq!(a.len(), 1);
2601        assert_eq!(a[0].len(), 2);
2602        assert!((a[0][0] - 0.0).abs() < 1e-6);
2603        assert!((a[0][1] - 1.0).abs() < 1e-6);
2604        assert!((b[0][0] - 0.2375).abs() < 1e-6);
2605        assert!((b[0][1] + 0.0593750).abs() < 1e-6);
2606    }
2607
2608    // ---------------- §5.7.7.3 interpolate ---------------------------
2609
2610    #[test]
2611    fn interpolate_smooth_one_param_set_linear_ramp() {
2612        // 1 param set, prev=0.0, p=4.0, num_ts=4 → ts=0 → 1.0, ts=3 → 4.0
2613        let p_set = vec![vec![4.0f32; NUM_QMF_SUBBANDS]];
2614        let prev = vec![0.0f32; NUM_QMF_SUBBANDS];
2615        let inp = InterpInputs {
2616            by_pset: &p_set,
2617            prev: &prev,
2618        };
2619        for ts in 0..4u32 {
2620            let v = interpolate(&inp, 1, 0, ts, 4, false, &[]);
2621            let expected = (ts + 1) as f32;
2622            assert!(
2623                (v - expected).abs() < 1e-6,
2624                "ts={ts} v={v} expected={expected}"
2625            );
2626        }
2627    }
2628
2629    #[test]
2630    fn interpolate_steep_falls_to_constants() {
2631        // 1 param set, steep, ts < timeslot[0] → prev, else current.
2632        let p_set = vec![vec![5.0f32; NUM_QMF_SUBBANDS]];
2633        let prev = vec![1.0f32; NUM_QMF_SUBBANDS];
2634        let inp = InterpInputs {
2635            by_pset: &p_set,
2636            prev: &prev,
2637        };
2638        // boundary at ts=2.
2639        assert_eq!(interpolate(&inp, 1, 0, 0, 4, true, &[2]), 1.0);
2640        assert_eq!(interpolate(&inp, 1, 0, 1, 4, true, &[2]), 1.0);
2641        assert_eq!(interpolate(&inp, 1, 0, 2, 4, true, &[2]), 5.0);
2642        assert_eq!(interpolate(&inp, 1, 0, 3, 4, true, &[2]), 5.0);
2643    }
2644
2645    #[test]
2646    fn interpolate_smooth_two_sets_meets_at_midpoint() {
2647        // 2 sets: prev=0, p0=4, p1=8, num_ts=8 → ts_2=4.
2648        // ts in [0,4): linear 0→4 over ts_2=4 → ts=0 → 1, ts=3 → 4.
2649        // ts in [4,8): linear 4→8 over (8-4)=4 → ts=4 → 5, ts=7 → 8.
2650        let p_set = vec![
2651            vec![4.0f32; NUM_QMF_SUBBANDS],
2652            vec![8.0f32; NUM_QMF_SUBBANDS],
2653        ];
2654        let prev = vec![0.0f32; NUM_QMF_SUBBANDS];
2655        let inp = InterpInputs {
2656            by_pset: &p_set,
2657            prev: &prev,
2658        };
2659        for ts in 0..8u32 {
2660            let v = interpolate(&inp, 2, 0, ts, 8, false, &[]);
2661            let expected = (ts + 1) as f32;
2662            assert!(
2663                (v - expected).abs() < 1e-5,
2664                "ts={ts} v={v} expected={expected}"
2665            );
2666        }
2667    }
2668
2669    // ---------------- expand_pb_to_sb -------------------------------
2670
2671    #[test]
2672    fn expand_pb_to_sb_preserves_first_pb_for_low_subbands_15() {
2673        // Table 197 column 15: sb=0 → pb=0, sb=8 → pb=8, sb=63 → pb=14.
2674        let pb = vec![vec![
2675            0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0,
2676        ]];
2677        let sb = expand_pb_to_sb(&pb, 15);
2678        assert_eq!(sb[0][0], 0.0);
2679        assert_eq!(sb[0][8], 8.0);
2680        assert_eq!(sb[0][9], 9.0);
2681        assert_eq!(sb[0][14], 11.0); // sb=14 → pb=11 (rows 14-17 → 11)
2682        assert_eq!(sb[0][63], 14.0);
2683    }
2684
2685    // ----------------- §5.7.7.4.2 Decorrelator -----------------------
2686
2687    #[test]
2688    fn region_delay_filter_length_match_table_198() {
2689        assert_eq!(region_delay(0), 7);
2690        assert_eq!(region_filter_length(0), 7);
2691        assert_eq!(region_delay(6), 7);
2692        assert_eq!(region_filter_length(6), 7);
2693        assert_eq!(region_delay(7), 10);
2694        assert_eq!(region_filter_length(7), 4);
2695        assert_eq!(region_delay(22), 10);
2696        assert_eq!(region_filter_length(22), 4);
2697        assert_eq!(region_delay(23), 12);
2698        assert_eq!(region_filter_length(23), 2);
2699        assert_eq!(region_delay(63), 12);
2700        assert_eq!(region_filter_length(63), 2);
2701    }
2702
2703    #[test]
2704    fn region_coeffs_match_tables_199_200_201_first_row() {
2705        assert_eq!(region_coeffs(DecorrelatorId::D0, 0)[0], 1.0);
2706        assert!((region_coeffs(DecorrelatorId::D0, 0)[1] - 0.5306).abs() < 1e-12);
2707        assert!((region_coeffs(DecorrelatorId::D1, 7)[1] - 0.0425).abs() < 1e-12);
2708        assert!((region_coeffs(DecorrelatorId::D2, 23)[1] + 0.6057).abs() < 1e-12);
2709    }
2710
2711    #[test]
2712    fn input_signal_modifier_zero_input_zero_output() {
2713        let mut m = InputSignalModifier::new(DecorrelatorId::D0);
2714        for sb in 0..NUM_QMF_SUBBANDS as u32 {
2715            let y = m.process_sample(sb, (0.0, 0.0));
2716            assert_eq!(y, (0.0, 0.0));
2717        }
2718    }
2719
2720    #[test]
2721    fn input_signal_modifier_impulse_eventually_yields_nonzero() {
2722        // Push an impulse at ts=0 into sb=0 and run for >7+7=14 slots.
2723        // The IIR response should produce non-zero output by ts ≥ delay.
2724        let mut m = InputSignalModifier::new(DecorrelatorId::D0);
2725        let mut energy = 0.0f64;
2726        for ts in 0..32 {
2727            let x = if ts == 0 { (1.0f32, 0.0) } else { (0.0, 0.0) };
2728            let y = m.process_sample(0, x);
2729            energy += (y.0 as f64).powi(2) + (y.1 as f64).powi(2);
2730        }
2731        assert!(energy > 0.0, "decorrelator should emit non-zero IIR tail");
2732    }
2733
2734    // ----------------- §5.7.7.4.3 Transient ducker -------------------
2735
2736    #[test]
2737    fn transient_ducker_passes_silence_unmodified() {
2738        let mut d = TransientDucker::new();
2739        let p_e = [0.0f32; ACPL_MAX_NUM_PARAM_BANDS];
2740        let g = d.update(&p_e);
2741        for v in g.iter() {
2742            assert_eq!(*v, 1.0, "silence should yield gain 1.0 (no transient)");
2743        }
2744    }
2745
2746    #[test]
2747    fn transient_ducker_attenuates_after_peak() {
2748        // Push a unit-energy spike followed by zero — the smoothed
2749        // decay should trigger ducking on the second slot.
2750        let mut d = TransientDucker::new();
2751        let mut spike = [0.0f32; ACPL_MAX_NUM_PARAM_BANDS];
2752        spike[0] = 1.0;
2753        let _ = d.update(&spike);
2754        // Now silence — peak_decay tail still > smooth, gamma branch
2755        // should fire and pull duck_gain[0] below 1.0.
2756        let zeros = [0.0f32; ACPL_MAX_NUM_PARAM_BANDS];
2757        let g = d.update(&zeros);
2758        assert!(g[0] < 1.0, "ducker should attenuate the post-peak slot");
2759        assert!(g[0] > 0.0);
2760    }
2761
2762    #[test]
2763    fn compute_p_energy_aggregates_per_param_band() {
2764        // Subband 0 → pb 0 in 15-band config. Magnitude 3 at sb 0 →
2765        // p_energy[0] = 9.
2766        let mut x = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
2767        x[0] = (3.0, 0.0);
2768        x[1] = (0.0, 4.0);
2769        let e = compute_p_energy(&x, 15);
2770        assert_eq!(e[0], 9.0);
2771        assert_eq!(e[1], 16.0);
2772        for pb in 2..ACPL_MAX_NUM_PARAM_BANDS {
2773            assert_eq!(e[pb], 0.0);
2774        }
2775    }
2776
2777    #[test]
2778    fn apply_transient_ducker_scales_per_pb() {
2779        let x = [(2.0f32, 0.0f32); NUM_QMF_SUBBANDS];
2780        let mut g = [1.0f32; ACPL_MAX_NUM_PARAM_BANDS];
2781        g[0] = 0.5;
2782        let out = apply_transient_ducker(&x, &g, 15);
2783        // sb 0 maps to pb 0 (gain 0.5) → 1.0, sb 1 maps to pb 1 (gain
2784        // 1.0) → 2.0.
2785        assert_eq!(out[0].0, 1.0);
2786        assert_eq!(out[1].0, 2.0);
2787    }
2788
2789    // ----------------- §5.7.7.5 acpl_module --------------------------
2790
2791    /// Synthetic stereo A-CPL frame: small sine signal in x0, zero in x1
2792    /// (ASPX_ACPL_2 mode), modest alpha/beta values, single param set.
2793    /// After running the synthesis we expect:
2794    ///   1) The output to differ from the input (cross-channel mixing
2795    ///      happened above acpl_qmf_band).
2796    ///   2) Below acpl_qmf_band the output is the M/S split of x0/x1.
2797    ///   3) The output stays within reasonable bounds (no NaN / inf).
2798    #[test]
2799    fn acpl_module_mixes_channels_above_qmf_band_aspx_acpl_2() {
2800        let num_ts = 32usize;
2801        let num_pb = 15u32;
2802        // Build x0 with a tone in subband 12 and 30, x1 silent.
2803        let mut x0 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
2804        let zero = [(0.0f32, 0.0f32); NUM_QMF_SUBBANDS];
2805        for ts in 0..num_ts {
2806            let phase = (ts as f32) * 0.5;
2807            x0[ts][12] = (phase.cos(), phase.sin());
2808            x0[ts][30] = (0.5 * phase.cos(), 0.5 * phase.sin());
2809        }
2810        // Synthetic decorrelator output y = x0 (just for the math test).
2811        let y: Vec<_> = x0.clone();
2812        // alpha = +0.5 across bands, beta = +0.5 across bands.
2813        let alpha_dq = vec![vec![0.5f32; num_pb as usize]];
2814        let beta_dq = vec![vec![0.5f32; num_pb as usize]];
2815        let frame = AcplCpeFrame {
2816            x0: &x0,
2817            x1: None,
2818            alpha_dq: &alpha_dq,
2819            beta_dq: &beta_dq,
2820            num_param_bands: num_pb,
2821            acpl_qmf_band: 8, // M/S below sb=8, mixed above
2822            steep: false,
2823            param_timeslots: &[],
2824        };
2825        let out = acpl_module(&frame, &y);
2826        assert_eq!(out.z0.len(), num_ts);
2827        assert_eq!(out.z1.len(), num_ts);
2828        // Above acpl_qmf_band, with x1=0 and y=x0, the formula is
2829        //   z0 = 0.5 * (x0 * (1+a) + x0 * b) = 0.5 * x0 * (1+a+b)
2830        // With smooth interp, num_pset=1, prev=0, p=0.5:
2831        //   interp(ts) = (ts+1)/num_ts * 0.5
2832        // At ts=0 → interp = 0.5/32 ≈ 0.0156, scale = 0.5*(1 + 2*0.0156)
2833        //          = 0.5156 → z0 = 0.5156 * x0 ≠ x0.
2834        // At ts=num_ts-1 → interp = 0.5, scale = 1.0 → z0 = x0 (boundary).
2835        let early = 0u32;
2836        let interp_early = 0.5_f32 * ((early + 1) as f32) / (num_ts as f32);
2837        let scale_early = 0.5_f32 * (1.0 + 2.0 * interp_early);
2838        let (z0r, z0i) = out.z0[early as usize][12];
2839        let (x0r, x0i) = x0[early as usize][12];
2840        let exp_re = scale_early * x0r;
2841        let exp_im = scale_early * x0i;
2842        assert!(
2843            (z0r - exp_re).abs() < 1e-5,
2844            "z0r {z0r} vs {exp_re} (scale {scale_early})"
2845        );
2846        assert!((z0i - exp_im).abs() < 1e-5, "z0i {z0i} vs {exp_im}");
2847        // Demonstrate divergence: at ts=0 z0 ≠ x0 because scale ≠ 1.0.
2848        assert!(
2849            (z0r - x0r).abs() > 1e-4 || (z0i - x0i).abs() > 1e-4,
2850            "z0[early][12] should differ from x0 (mixing scale != 1.0) — got z0=({z0r},{z0i}) x0=({x0r},{x0i}) scale={scale_early}"
2851        );
2852        // Below acpl_qmf_band (sb < 8): with x1 = 0, z0 = 0.5*x0,
2853        // z1 = 0.5*x0.
2854        for sb in 0..8 {
2855            for ts in 0..num_ts {
2856                let (z0r, z0i) = out.z0[ts][sb];
2857                let (z1r, z1i) = out.z1[ts][sb];
2858                assert!((z0r - 0.5 * x0[ts][sb].0).abs() < 1e-6);
2859                assert!((z0i - 0.5 * x0[ts][sb].1).abs() < 1e-6);
2860                assert!((z1r - 0.5 * x0[ts][sb].0).abs() < 1e-6);
2861                assert!((z1i - 0.5 * x0[ts][sb].1).abs() < 1e-6);
2862            }
2863        }
2864        // No NaN / Inf in output.
2865        for ts in 0..num_ts {
2866            for sb in 0..NUM_QMF_SUBBANDS {
2867                assert!(out.z0[ts][sb].0.is_finite());
2868                assert!(out.z0[ts][sb].1.is_finite());
2869                assert!(out.z1[ts][sb].0.is_finite());
2870                assert!(out.z1[ts][sb].1.is_finite());
2871            }
2872        }
2873        // Silence the unused y matrix to suppress warnings.
2874        let _ = zero;
2875    }
2876
2877    /// ASPX_ACPL_1: x1 ≠ 0 — stereo input. Verify the M/S split below
2878    /// `acpl_qmf_band` correctly produces (x0+x1)/2 and (x0-x1)/2.
2879    #[test]
2880    fn acpl_module_below_qmf_band_is_mid_side_split() {
2881        let num_ts = 8usize;
2882        let mut x0 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
2883        let mut x1 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
2884        for ts in 0..num_ts {
2885            x0[ts][2] = (1.0, 0.0);
2886            x1[ts][2] = (0.0, 1.0);
2887        }
2888        let y = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
2889        let alpha_dq = vec![vec![0.0f32; 15]];
2890        let beta_dq = vec![vec![0.0f32; 15]];
2891        let frame = AcplCpeFrame {
2892            x0: &x0,
2893            x1: Some(&x1),
2894            alpha_dq: &alpha_dq,
2895            beta_dq: &beta_dq,
2896            num_param_bands: 15,
2897            acpl_qmf_band: 4,
2898            steep: false,
2899            param_timeslots: &[],
2900        };
2901        let out = acpl_module(&frame, &y);
2902        for ts in 0..num_ts {
2903            let z0 = out.z0[ts][2];
2904            let z1 = out.z1[ts][2];
2905            // (1+0j + 0+1j)/2 = (0.5, 0.5)
2906            assert!((z0.0 - 0.5).abs() < 1e-6);
2907            assert!((z0.1 - 0.5).abs() < 1e-6);
2908            // (1+0j - (0+1j))/2 = (0.5, -0.5)
2909            assert!((z1.0 - 0.5).abs() < 1e-6);
2910            assert!((z1.1 + 0.5).abs() < 1e-6);
2911        }
2912    }
2913
2914    /// End-to-end check: run the full Pseudocode 115 pipeline (decorr
2915    /// + ducker + acpl_module) on a non-trivial frame. Verify nothing
2916    ///   blows up and the output differs from passthrough.
2917    #[test]
2918    fn run_pseudocode_115_pair_end_to_end() {
2919        let num_ts = 16usize;
2920        let num_pb = 9u32;
2921        let mut x0 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
2922        for ts in 0..num_ts {
2923            let phase = (ts as f32) * 0.3;
2924            for sb in 0..32 {
2925                x0[ts][sb] = (
2926                    0.1 * (phase + sb as f32 * 0.1).cos(),
2927                    0.1 * (phase + sb as f32 * 0.1).sin(),
2928                );
2929            }
2930        }
2931        let alpha_dq = vec![vec![0.3f32; num_pb as usize]];
2932        let beta_dq = vec![vec![0.3f32; num_pb as usize]];
2933        let mut state = AcplCpeState::new(DecorrelatorId::D0);
2934        let frame = AcplCpeFrame {
2935            x0: &x0,
2936            x1: None,
2937            alpha_dq: &alpha_dq,
2938            beta_dq: &beta_dq,
2939            num_param_bands: num_pb,
2940            acpl_qmf_band: 4,
2941            steep: false,
2942            param_timeslots: &[],
2943        };
2944        let out = run_pseudocode_115_pair(&mut state, frame);
2945        assert_eq!(out.z0.len(), num_ts);
2946        assert_eq!(out.z1.len(), num_ts);
2947        // Output should differ from the passthrough below acpl_qmf_band
2948        // (we doubled x0 inside).
2949        let mut diff_energy = 0.0f64;
2950        for ts in 0..num_ts {
2951            for sb in 0..NUM_QMF_SUBBANDS {
2952                assert!(out.z0[ts][sb].0.is_finite());
2953                assert!(out.z0[ts][sb].1.is_finite());
2954                assert!(out.z1[ts][sb].0.is_finite());
2955                assert!(out.z1[ts][sb].1.is_finite());
2956                let dr = out.z0[ts][sb].0 - x0[ts][sb].0;
2957                let di = out.z0[ts][sb].1 - x0[ts][sb].1;
2958                diff_energy += (dr as f64).powi(2) + (di as f64).powi(2);
2959            }
2960        }
2961        assert!(diff_energy > 0.0, "output should diverge from x0");
2962        // After a frame, prev arrays should be populated.
2963        assert_eq!(state.alpha_prev_sb.len(), NUM_QMF_SUBBANDS);
2964        assert_eq!(state.beta_prev_sb.len(), NUM_QMF_SUBBANDS);
2965        // alpha_dq=0.3 across pb → expand_pb_to_sb fills sb with 0.3.
2966        for sb in 0..NUM_QMF_SUBBANDS {
2967            assert!((state.alpha_prev_sb[sb] - 0.3).abs() < 1e-6);
2968            assert!((state.beta_prev_sb[sb] - 0.3).abs() < 1e-6);
2969        }
2970    }
2971
2972    /// End-to-end: alpha=0, beta=0 with x1 = x0 → above acpl_qmf_band
2973    /// the formula collapses to z0 = 0.5*x0in = x0 (since x0in = 2*x0).
2974    #[test]
2975    fn acpl_module_zero_params_passthrough_above_qmf_band() {
2976        let num_ts = 4usize;
2977        let mut x0in = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
2978        for ts in 0..num_ts {
2979            x0in[ts][20] = (2.0, 1.0); // already-doubled x0
2980        }
2981        let y = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
2982        let alpha_dq = vec![vec![0.0f32; 15]];
2983        let beta_dq = vec![vec![0.0f32; 15]];
2984        let frame = AcplCpeFrame {
2985            x0: &x0in,
2986            x1: None,
2987            alpha_dq: &alpha_dq,
2988            beta_dq: &beta_dq,
2989            num_param_bands: 15,
2990            acpl_qmf_band: 8,
2991            steep: false,
2992            param_timeslots: &[],
2993        };
2994        let out = acpl_module(&frame, &y);
2995        // Above qmf_band (sb=20), a=b=0, y=0 → z0 = 0.5*x0*(1+0) = 0.5*x0in
2996        // = (1.0, 0.5)
2997        for ts in 0..num_ts {
2998            let z = out.z0[ts][20];
2999            assert!((z.0 - 1.0).abs() < 1e-6);
3000            assert!((z.1 - 0.5).abs() < 1e-6);
3001            // z1 = 0.5*x0*(1-0) = 0.5*x0 = (1.0, 0.5)
3002            let z1 = out.z1[ts][20];
3003            assert!((z1.0 - 1.0).abs() < 1e-6);
3004            assert!((z1.1 - 0.5).abs() < 1e-6);
3005        }
3006    }
3007
3008    // ----------------- run_acpl_1ch_pcm — PCM end-to-end --------------
3009
3010    #[test]
3011    fn run_acpl_1ch_pcm_emits_two_channel_pcm_with_energy() {
3012        // End-to-end smoke test: feed a synthetic mono sine into the
3013        // §5.7.7.5 channel-pair pipeline (QMF analysis → A-CPL → QMF
3014        // synthesis) and assert both output channels carry energy and
3015        // come out the right length.
3016        use crate::acpl::{
3017            Acpl1chMode, AcplConfig1ch, AcplData1ch, AcplFramingData, AcplHuffParam,
3018            AcplInterpolationType, AcplQuantMode,
3019        };
3020        let _ = Acpl1chMode::Full; // silence unused-import warning
3021        let n_slots = 32usize;
3022        let n = n_slots * NUM_QMF_SUBBANDS;
3023        let mut pcm = vec![0.0f32; n];
3024        let f = 440.0_f32 / 48_000.0_f32;
3025        for (i, s) in pcm.iter_mut().enumerate() {
3026            *s = (2.0 * std::f32::consts::PI * f * i as f32).sin();
3027        }
3028        let cfg = AcplConfig1ch {
3029            num_param_bands_id: 0,
3030            num_param_bands: 15,
3031            quant_mode: AcplQuantMode::Fine,
3032            qmf_band: 0,
3033        };
3034        // Hand-built parsed acpl_data_1ch — single param set, alpha
3035        // and beta carrying the F0 anchor index (huffman F0 cb_off
3036        // gives signed 0 here).
3037        let alpha = AcplHuffParam {
3038            values: vec![4i32; cfg.num_param_bands as usize],
3039            direction_time: false,
3040        };
3041        let beta = AcplHuffParam {
3042            values: vec![2i32; cfg.num_param_bands as usize],
3043            direction_time: false,
3044        };
3045        let data = AcplData1ch {
3046            framing: AcplFramingData {
3047                interpolation_type: AcplInterpolationType::Smooth,
3048                num_param_sets_cod: 0,
3049                num_param_sets: 1,
3050                param_timeslots: vec![],
3051            },
3052            alpha1: vec![alpha],
3053            beta1: vec![beta],
3054        };
3055        let mut state = AcplSubstreamState::new();
3056        let (left, right) = run_acpl_1ch_pcm(&pcm, &cfg, &data, &mut state).expect("synth runs");
3057        assert_eq!(left.len(), pcm.len());
3058        assert_eq!(right.len(), pcm.len());
3059        // Both channels should carry energy in the steady-state tail.
3060        let start = 1024usize;
3061        let e_l: f64 = left[start..].iter().map(|&s| (s as f64).powi(2)).sum();
3062        let e_r: f64 = right[start..].iter().map(|&s| (s as f64).powi(2)).sum();
3063        assert!(e_l > 1e-6, "left channel silent (e={e_l})");
3064        assert!(e_r > 1e-6, "right channel silent (e={e_r})");
3065        // The two channels must differ — a non-zero alpha drives them
3066        // apart.
3067        let mut diffs = 0usize;
3068        for (l, r) in left[start..].iter().zip(right[start..].iter()) {
3069            if (l - r).abs() > 1e-6 {
3070                diffs += 1;
3071            }
3072        }
3073        assert!(
3074            diffs > (left.len() - start) / 4,
3075            "channels too similar (diffs={diffs})"
3076        );
3077    }
3078
3079    #[test]
3080    fn run_acpl_1ch_pcm_stereo_emits_two_channels_distinct_from_l_r_passthrough() {
3081        // Stereo (M/S) input variant — same shape as ACPL_2 but with x1
3082        // populated. Verifies the spec's M/S split path runs end-to-end
3083        // and produces non-silent, distinct channels.
3084        use crate::acpl::{
3085            AcplConfig1ch, AcplData1ch, AcplFramingData, AcplHuffParam, AcplInterpolationType,
3086            AcplQuantMode,
3087        };
3088        let n_slots = 32usize;
3089        let n = n_slots * NUM_QMF_SUBBANDS;
3090        let mut pcm_m = vec![0.0f32; n];
3091        let mut pcm_s = vec![0.0f32; n];
3092        let f_m = 440.0_f32 / 48_000.0_f32;
3093        let f_s = 220.0_f32 / 48_000.0_f32;
3094        for i in 0..n {
3095            pcm_m[i] = (2.0 * std::f32::consts::PI * f_m * i as f32).sin();
3096            pcm_s[i] = 0.3 * (2.0 * std::f32::consts::PI * f_s * i as f32).sin();
3097        }
3098        let cfg = AcplConfig1ch {
3099            num_param_bands_id: 0,
3100            num_param_bands: 15,
3101            quant_mode: AcplQuantMode::Fine,
3102            // PARTIAL mode: nonzero qmf_band so the M/S split path
3103            // engages on low subbands.
3104            qmf_band: 8,
3105        };
3106        let alpha = AcplHuffParam {
3107            values: vec![4i32; cfg.num_param_bands as usize],
3108            direction_time: false,
3109        };
3110        let beta = AcplHuffParam {
3111            values: vec![2i32; cfg.num_param_bands as usize],
3112            direction_time: false,
3113        };
3114        let data = AcplData1ch {
3115            framing: AcplFramingData {
3116                interpolation_type: AcplInterpolationType::Smooth,
3117                num_param_sets_cod: 0,
3118                num_param_sets: 1,
3119                param_timeslots: vec![],
3120            },
3121            alpha1: vec![alpha],
3122            beta1: vec![beta],
3123        };
3124        let mut state = AcplSubstreamState::new();
3125        let (left, right) = run_acpl_1ch_pcm_stereo(&pcm_m, &pcm_s, &cfg, &data, &mut state)
3126            .expect("stereo synth runs");
3127        assert_eq!(left.len(), pcm_m.len());
3128        assert_eq!(right.len(), pcm_m.len());
3129        let start = 1024usize;
3130        let e_l: f64 = left[start..].iter().map(|&s| (s as f64).powi(2)).sum();
3131        let e_r: f64 = right[start..].iter().map(|&s| (s as f64).powi(2)).sum();
3132        assert!(e_l > 1e-6, "left channel silent (e={e_l})");
3133        assert!(e_r > 1e-6, "right channel silent (e={e_r})");
3134        // The output must not be a literal pass-through of the input.
3135        let mut diff_from_input = 0usize;
3136        for i in start..left.len() {
3137            if (left[i] - pcm_m[i]).abs() > 1e-3 {
3138                diff_from_input += 1;
3139            }
3140        }
3141        assert!(
3142            diff_from_input > (left.len() - start) / 4,
3143            "left channel matches input PCM (diff_from_input={diff_from_input})"
3144        );
3145    }
3146
3147    #[test]
3148    fn run_acpl_1ch_pcm_stereo_rejects_mismatched_lengths() {
3149        use crate::acpl::{
3150            AcplConfig1ch, AcplData1ch, AcplFramingData, AcplInterpolationType, AcplQuantMode,
3151        };
3152        let pcm_m = vec![0.0f32; 64];
3153        let pcm_s = vec![0.0f32; 128];
3154        let cfg = AcplConfig1ch {
3155            num_param_bands_id: 0,
3156            num_param_bands: 15,
3157            quant_mode: AcplQuantMode::Fine,
3158            qmf_band: 0,
3159        };
3160        let data = AcplData1ch {
3161            framing: AcplFramingData {
3162                interpolation_type: AcplInterpolationType::Smooth,
3163                num_param_sets_cod: 0,
3164                num_param_sets: 1,
3165                param_timeslots: vec![],
3166            },
3167            alpha1: vec![],
3168            beta1: vec![],
3169        };
3170        let mut state = AcplSubstreamState::new();
3171        assert!(run_acpl_1ch_pcm_stereo(&pcm_m, &pcm_s, &cfg, &data, &mut state).is_none());
3172    }
3173
3174    #[test]
3175    fn run_acpl_1ch_pcm_rejects_misaligned_pcm() {
3176        use crate::acpl::{
3177            AcplConfig1ch, AcplData1ch, AcplFramingData, AcplInterpolationType, AcplQuantMode,
3178        };
3179        let pcm = vec![0.0f32; 65]; // not a multiple of 64
3180        let cfg = AcplConfig1ch {
3181            num_param_bands_id: 0,
3182            num_param_bands: 15,
3183            quant_mode: AcplQuantMode::Fine,
3184            qmf_band: 0,
3185        };
3186        let data = AcplData1ch {
3187            framing: AcplFramingData {
3188                interpolation_type: AcplInterpolationType::Smooth,
3189                num_param_sets_cod: 0,
3190                num_param_sets: 1,
3191                param_timeslots: vec![],
3192            },
3193            alpha1: vec![],
3194            beta1: vec![],
3195        };
3196        let mut state = AcplSubstreamState::new();
3197        assert!(run_acpl_1ch_pcm(&pcm, &cfg, &data, &mut state).is_none());
3198    }
3199
3200    // =====================================================================
3201    // §5.7.7.6.2 — ASPX_ACPL_3 multichannel transform synthesis tests
3202    // (Pseudocodes 117 / 118 / 119 wiring + helpers)
3203    // =====================================================================
3204
3205    /// `Transform()` with `g1 = 1, g2 = 0` should pass `x0` through and
3206    /// drop `x1`. `g1 = 0, g2 = 1` should pass `x1` through.
3207    #[test]
3208    fn transform_unit_gammas_select_carriers() {
3209        let num_ts = 8usize;
3210        let mut x0 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3211        let mut x1 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3212        for ts in 0..num_ts {
3213            x0[ts][10] = (1.0, 0.0);
3214            x1[ts][10] = (0.0, 1.0);
3215        }
3216        let g1 = vec![vec![1.0f32; 15]];
3217        let g2 = vec![vec![0.0f32; 15]];
3218        let prev = vec![1.0f32; NUM_QMF_SUBBANDS];
3219        let prev_zero = vec![0.0f32; NUM_QMF_SUBBANDS];
3220        // num_pset == 1, prev_g1 = 1.0 across sb -> interpolation stays
3221        // at 1.0 (start at prev=1.0, target=1.0, no ramp).
3222        let v = transform(&x0, &x1, &g1, &g2, 15, &prev, &prev_zero, false, &[]);
3223        for ts in 0..num_ts {
3224            // sb=10: v = x0 * 1 + x1 * 0 = x0.
3225            let (vr, vi) = v[ts][10];
3226            assert!((vr - 1.0).abs() < 1e-5, "ts={ts} vr={vr}, expected x0=1.0");
3227            assert!((vi - 0.0).abs() < 1e-5, "ts={ts} vi={vi}");
3228            // sb=20: both x0 and x1 are zero, result must be zero.
3229            assert_eq!(v[ts][20], (0.0, 0.0));
3230        }
3231
3232        // Now flip: g1 = 0 → x1 carries through.
3233        let g1 = vec![vec![0.0f32; 15]];
3234        let g2 = vec![vec![1.0f32; 15]];
3235        let prev_g2 = vec![1.0f32; NUM_QMF_SUBBANDS];
3236        let v = transform(&x0, &x1, &g1, &g2, 15, &prev_zero, &prev_g2, false, &[]);
3237        for ts in 0..num_ts {
3238            let (vr, vi) = v[ts][10];
3239            assert!((vr - 0.0).abs() < 1e-5);
3240            assert!((vi - 1.0).abs() < 1e-5);
3241        }
3242    }
3243
3244    /// `Transform()` with mixed gammas should produce the linear
3245    /// combination `x0*g1 + x1*g2` after the smooth-interpolation ramp.
3246    #[test]
3247    fn transform_mixes_two_carriers_with_gammas() {
3248        let num_ts = 4usize;
3249        let mut x0 = vec![[(2.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3250        let mut x1 = vec![[(0.0f32, 3.0f32); NUM_QMF_SUBBANDS]; num_ts];
3251        // Make g1 = 0.5 across all bands, g2 = 0.25. With prev = same
3252        // values, smooth interpolation collapses to constant gammas.
3253        let g1 = vec![vec![0.5f32; 15]];
3254        let g2 = vec![vec![0.25f32; 15]];
3255        let prev_g1 = vec![0.5f32; NUM_QMF_SUBBANDS];
3256        let prev_g2 = vec![0.25f32; NUM_QMF_SUBBANDS];
3257        let v = transform(&x0, &x1, &g1, &g2, 15, &prev_g1, &prev_g2, false, &[]);
3258        for ts in 0..num_ts {
3259            for sb in 0..NUM_QMF_SUBBANDS {
3260                let (vr, vi) = v[ts][sb];
3261                // x0 contributes (2 * 0.5, 0). x1 contributes (0, 3 * 0.25).
3262                assert!((vr - 1.0).abs() < 1e-5, "ts={ts} sb={sb} vr={vr}");
3263                assert!((vi - 0.75).abs() < 1e-5, "ts={ts} sb={sb} vi={vi}");
3264            }
3265        }
3266        // Quiet unused-mut warnings.
3267        let _ = (&mut x0, &mut x1);
3268    }
3269
3270    /// `acpl_module2()` with g1=g2=g1*a=g2*a=b=0 must yield silent z0,z1.
3271    #[test]
3272    fn acpl_module2_zero_gammas_is_silent() {
3273        let num_ts = 4usize;
3274        let x0 = vec![[(1.0f32, 0.5f32); NUM_QMF_SUBBANDS]; num_ts];
3275        let x1 = vec![[(0.5f32, 1.0f32); NUM_QMF_SUBBANDS]; num_ts];
3276        let y = vec![[(2.0f32, 2.0f32); NUM_QMF_SUBBANDS]; num_ts];
3277        let zero = vec![vec![0.0f32; 15]];
3278        let (z0, z1) = acpl_module2(
3279            &x0,
3280            &x1,
3281            &y,
3282            &zero,
3283            &zero,
3284            &zero,
3285            &zero,
3286            &zero,
3287            15,
3288            false,
3289            &[],
3290        );
3291        for ts in 0..num_ts {
3292            for sb in 0..NUM_QMF_SUBBANDS {
3293                assert_eq!(z0[ts][sb], (0.0, 0.0));
3294                assert_eq!(z1[ts][sb], (0.0, 0.0));
3295            }
3296        }
3297    }
3298
3299    /// `acpl_module2()` with g1=1, g2=0, a=0, b=0 (so g1a = g2a = 0):
3300    ///   z0 = 0.5 * (x0 * (g1+g1a) + x1 * (g2+g2a) + y*b)
3301    ///       = 0.5 * x0 * 1
3302    ///   z1 = 0.5 * x0 * (1-0) = 0.5 * x0
3303    /// → both z0 and z1 collapse to 0.5*x0.
3304    #[test]
3305    fn acpl_module2_unit_g1_zero_alpha_beta_passes_half_x0_to_both() {
3306        let num_ts = 4usize;
3307        let mut x0 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3308        let x1 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3309        let y = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3310        for ts in 0..num_ts {
3311            x0[ts][5] = (4.0, 2.0);
3312        }
3313        let g1 = vec![vec![1.0f32; 15]];
3314        let g2 = vec![vec![0.0f32; 15]];
3315        let g1a = vec![vec![0.0f32; 15]]; // a=0 → g1*a=0
3316        let g2a = vec![vec![0.0f32; 15]];
3317        let b = vec![vec![0.0f32; 15]];
3318        let (z0, z1) = acpl_module2(&x0, &x1, &y, &g1, &g2, &g1a, &g2a, &b, 15, false, &[]);
3319        // smooth interpolation with prev=0, target=1 → at ts=num_ts-1, g1
3320        // hits 1.0, but z formula has g1+g1a so 1+0=1; z = 0.5*x0*1 = 0.5*x0.
3321        // Just assert the last ts converges.
3322        let last = num_ts - 1;
3323        let (z0r, z0i) = z0[last][5];
3324        let (z1r, z1i) = z1[last][5];
3325        assert!((z0r - 2.0).abs() < 1e-5, "z0r={z0r} expected 2.0");
3326        assert!((z0i - 1.0).abs() < 1e-5, "z0i={z0i} expected 1.0");
3327        assert!((z1r - 2.0).abs() < 1e-5, "z1r={z1r}");
3328        assert!((z1i - 1.0).abs() < 1e-5, "z1i={z1i}");
3329    }
3330
3331    /// `acpl_module3()` adds a beta3-driven decorrelator residual.
3332    /// With b3=1, b3a=0 (i.e. a=0): z0 += 0.25*y*1 = 0.25*y, z1 += same.
3333    #[test]
3334    fn acpl_module3_adds_decorrelator_residual() {
3335        let num_ts = 4usize;
3336        let mut z0 = vec![[(1.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3337        let mut z1 = vec![[(0.0f32, 1.0f32); NUM_QMF_SUBBANDS]; num_ts];
3338        let y2 = vec![[(4.0f32, 4.0f32); NUM_QMF_SUBBANDS]; num_ts];
3339        let b3 = vec![vec![1.0f32; 15]];
3340        let b3a = vec![vec![0.0f32; 15]];
3341        acpl_module3(&mut z0, &mut z1, &y2, &b3, &b3a, 15, false, &[]);
3342        // After ramp completes (ts=num_ts-1) b3 = 1.0, b3a = 0:
3343        //   z0 += 0.25 * y2 * (1 + 0) = 0.25 * (4, 4) = (1, 1)
3344        //   z1 += 0.25 * y2 * (1 - 0) = (1, 1)
3345        let last = num_ts - 1;
3346        let (z0r, z0i) = z0[last][7];
3347        assert!((z0r - 2.0).abs() < 1e-5, "z0r={z0r}");
3348        assert!((z0i - 1.0).abs() < 1e-5, "z0i={z0i}");
3349        let (z1r, z1i) = z1[last][7];
3350        assert!((z1r - 1.0).abs() < 1e-5, "z1r={z1r}");
3351        assert!((z1i - 2.0).abs() < 1e-5, "z1i={z1i}");
3352    }
3353
3354    /// `acpl_module3()` with all-zero beta3 is a no-op.
3355    #[test]
3356    fn acpl_module3_zero_beta3_is_noop() {
3357        let num_ts = 4usize;
3358        let mut z0 = vec![[(7.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3359        let mut z1 = vec![[(0.0f32, 7.0f32); NUM_QMF_SUBBANDS]; num_ts];
3360        let y2 = vec![[(99.0f32, -99.0f32); NUM_QMF_SUBBANDS]; num_ts];
3361        let zero = vec![vec![0.0f32; 15]];
3362        acpl_module3(&mut z0, &mut z1, &y2, &zero, &zero, 15, false, &[]);
3363        for ts in 0..num_ts {
3364            for sb in 0..NUM_QMF_SUBBANDS {
3365                assert_eq!(z0[ts][sb], (7.0, 0.0));
3366                assert_eq!(z1[ts][sb], (0.0, 7.0));
3367            }
3368        }
3369    }
3370
3371    /// Smoke test for the full §5.7.7.6.2 ASPX_ACPL_3 pipeline. Feed
3372    /// non-zero L/R/C carriers and verify all 5 outputs are populated and
3373    /// finite.
3374    #[test]
3375    fn run_pseudocode_118_5x_emits_five_finite_channels() {
3376        let num_ts = 16usize;
3377        let num_pb = 9u32;
3378        let mut x0 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3379        let mut x1 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3380        let mut x2 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3381        for ts in 0..num_ts {
3382            for sb in 0..32 {
3383                let p = (ts as f32) * 0.2 + sb as f32 * 0.05;
3384                x0[ts][sb] = (0.1 * p.cos(), 0.1 * p.sin());
3385                x1[ts][sb] = (0.05 * p.sin(), 0.05 * p.cos());
3386                x2[ts][sb] = (0.03 * (p * 1.3).cos(), 0.0);
3387            }
3388        }
3389        let alpha_1 = vec![vec![0.4f32; num_pb as usize]];
3390        let alpha_2 = vec![vec![-0.2f32; num_pb as usize]];
3391        let beta_1 = vec![vec![0.3f32; num_pb as usize]];
3392        let beta_2 = vec![vec![0.2f32; num_pb as usize]];
3393        let beta_3 = vec![vec![0.15f32; num_pb as usize]];
3394        let g1 = vec![vec![0.6f32; num_pb as usize]];
3395        let g2 = vec![vec![0.3f32; num_pb as usize]];
3396        let g3 = vec![vec![0.2f32; num_pb as usize]];
3397        let g4 = vec![vec![0.5f32; num_pb as usize]];
3398        let g5 = vec![vec![0.1f32; num_pb as usize]];
3399        let g6 = vec![vec![0.1f32; num_pb as usize]];
3400        let mut state = AcplMchState::new();
3401        let frame = AcplMchFrame {
3402            x0: &x0,
3403            x1: &x1,
3404            x2: &x2,
3405            alpha_1_dq: &alpha_1,
3406            alpha_2_dq: &alpha_2,
3407            beta_1_dq: &beta_1,
3408            beta_2_dq: &beta_2,
3409            beta_3_dq: &beta_3,
3410            g1_dq: &g1,
3411            g2_dq: &g2,
3412            g3_dq: &g3,
3413            g4_dq: &g4,
3414            g5_dq: &g5,
3415            g6_dq: &g6,
3416            num_param_bands: num_pb,
3417            steep: false,
3418            param_timeslots: &[],
3419        };
3420        let out = run_pseudocode_118_5x(&mut state, frame);
3421        assert_eq!(out.z0.len(), num_ts);
3422        assert_eq!(out.z1.len(), num_ts);
3423        assert_eq!(out.z2.len(), num_ts);
3424        assert_eq!(out.z3.len(), num_ts);
3425        assert_eq!(out.z4.len(), num_ts);
3426        // No NaN / Inf in any output channel.
3427        for z in [&out.z0, &out.z1, &out.z2, &out.z3, &out.z4] {
3428            for col in z.iter() {
3429                for sb in 0..NUM_QMF_SUBBANDS {
3430                    assert!(
3431                        col[sb].0.is_finite() && col[sb].1.is_finite(),
3432                        "non-finite output: {:?}",
3433                        col[sb]
3434                    );
3435                }
3436            }
3437        }
3438        // Carriers are non-zero; the synthesis must produce non-zero
3439        // energy on all five channels.
3440        for (name, z) in [
3441            ("z0=L", &out.z0),
3442            ("z1=Ls", &out.z1),
3443            ("z2=R", &out.z2),
3444            ("z3=Rs", &out.z3),
3445            ("z4=C", &out.z4),
3446        ] {
3447            let e: f64 = z
3448                .iter()
3449                .flat_map(|col| col.iter())
3450                .map(|s| (s.0 as f64).powi(2) + (s.1 as f64).powi(2))
3451                .sum();
3452            assert!(e > 0.0, "channel {name} silent (energy {e})");
3453        }
3454        // Gamma prev arrays should be populated after the call.
3455        assert_eq!(state.g1_prev_sb.len(), NUM_QMF_SUBBANDS);
3456        for sb in 0..NUM_QMF_SUBBANDS {
3457            assert!((state.g1_prev_sb[sb] - 0.6).abs() < 1e-6);
3458        }
3459    }
3460
3461    /// All-zero alpha/beta/beta3 + matching gamma should still produce
3462    /// valid, finite output. This covers the "no decorrelator coupling"
3463    /// path through the 5_X synthesis.
3464    #[test]
3465    fn run_pseudocode_118_5x_zero_alpha_beta_remains_finite() {
3466        let num_ts = 8usize;
3467        let num_pb = 9u32;
3468        let x0 = vec![[(0.5f32, 0.5f32); NUM_QMF_SUBBANDS]; num_ts];
3469        let x1 = vec![[(0.5f32, -0.5f32); NUM_QMF_SUBBANDS]; num_ts];
3470        let x2 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3471        let zero = vec![vec![0.0f32; num_pb as usize]];
3472        let g1 = vec![vec![1.0f32; num_pb as usize]]; // g1 != 0 so v1 isn't trivially silent
3473        let mut state = AcplMchState::new();
3474        let frame = AcplMchFrame {
3475            x0: &x0,
3476            x1: &x1,
3477            x2: &x2,
3478            alpha_1_dq: &zero,
3479            alpha_2_dq: &zero,
3480            beta_1_dq: &zero,
3481            beta_2_dq: &zero,
3482            beta_3_dq: &zero,
3483            g1_dq: &g1,
3484            g2_dq: &zero,
3485            g3_dq: &zero,
3486            g4_dq: &zero,
3487            g5_dq: &zero,
3488            g6_dq: &zero,
3489            num_param_bands: num_pb,
3490            steep: false,
3491            param_timeslots: &[],
3492        };
3493        let out = run_pseudocode_118_5x(&mut state, frame);
3494        for col in out.z0.iter() {
3495            for sb in 0..NUM_QMF_SUBBANDS {
3496                assert!(col[sb].0.is_finite());
3497                assert!(col[sb].1.is_finite());
3498            }
3499        }
3500    }
3501
3502    /// Smoke test for `pb_matrix_*` helpers used inside Pseudocode 118.
3503    #[test]
3504    fn pb_matrix_helpers() {
3505        let a = vec![vec![1.0f32, 2.0, 3.0]];
3506        let b = vec![vec![10.0f32, 20.0, 30.0]];
3507        let c = vec![vec![100.0f32, 200.0, 300.0]];
3508        let prod = pb_matrix_mul(&a, &b);
3509        assert_eq!(prod, vec![vec![10.0, 40.0, 90.0]]);
3510        let sum = pb_matrix_sum3(&a, &b, &c);
3511        assert_eq!(sum, vec![vec![111.0, 222.0, 333.0]]);
3512        let scaled = pb_matrix_scale(&a, -2.0);
3513        assert_eq!(scaled, vec![vec![-2.0, -4.0, -6.0]]);
3514    }
3515
3516    /// Spec consistency: the `(1 + 2*sqrt(0.5))` scaling factor applied
3517    /// to x0/x1 in Pseudocode 118 must equal `1 + sqrt(2)` ≈ 2.414...
3518    #[test]
3519    fn pseudocode_118_scaling_factor() {
3520        let s = 1.0f32 + 2.0 * (0.5f32).sqrt();
3521        let expected = 1.0f32 + (2.0f32).sqrt();
3522        assert!((s - expected).abs() < 1e-6);
3523    }
3524
3525    /// `AcplMchState::new()` should initialise prev arrays to zero.
3526    #[test]
3527    fn acpl_mch_state_zero_init() {
3528        let s = AcplMchState::new();
3529        assert!(s.g1_prev_sb.iter().all(|&v| v == 0.0));
3530        assert!(s.g2_prev_sb.iter().all(|&v| v == 0.0));
3531        assert!(s.g3_prev_sb.iter().all(|&v| v == 0.0));
3532        assert!(s.g4_prev_sb.iter().all(|&v| v == 0.0));
3533        assert_eq!(s.g1_prev_sb.len(), NUM_QMF_SUBBANDS);
3534    }
3535
3536    // =====================================================================
3537    // §5.7.7.6.1 — ASPX_ACPL_1 / ASPX_ACPL_2 multichannel wrapper tests
3538    // (Pseudocode 117)
3539    // =====================================================================
3540
3541    /// `Acpl5xPairState::new()` should set up D0 on the L-side and D1 on
3542    /// the R-side, with zero prev arrays + diff state.
3543    #[test]
3544    fn acpl_5x_pair_state_initial_decorrelators() {
3545        let s = Acpl5xPairState::new();
3546        assert!(matches!(s.left_pair.decorrelator.which, DecorrelatorId::D0));
3547        assert!(matches!(
3548            s.right_pair.decorrelator.which,
3549            DecorrelatorId::D1
3550        ));
3551        assert!(s.left_pair.alpha_prev_sb.iter().all(|&v| v == 0.0));
3552        assert!(s.right_pair.alpha_prev_sb.iter().all(|&v| v == 0.0));
3553        assert!(s.alpha1_diff.q_prev.is_empty());
3554        assert!(s.alpha2_diff.q_prev.is_empty());
3555    }
3556
3557    /// Build a small synthetic 5_X frame with x0/x1 carrying tones and
3558    /// the centre channel carrying a distinct tone. Run Pseudocode 117
3559    /// in `ASPX_ACPL_2` mode (no Ls/Rs carriers) and verify that:
3560    ///
3561    /// 1. Five output matrices are produced, all `num_ts × 64` shaped.
3562    /// 2. The centre output `z4` matches `x2` byte-for-byte (passthrough).
3563    /// 3. `z1` / `z3` are scaled by sqrt(2) (the M/S split below
3564    ///    `acpl_qmf_band` is `0.5*(x0±0)*sqrt(2)` for the surround pair).
3565    /// 4. No NaN / Inf in any output sample.
3566    #[test]
3567    fn run_pseudocode_117_5x_aspx_acpl_2_passthrough_centre_and_finite() {
3568        let num_ts = 16usize;
3569        let num_pb = 9u32;
3570        let mut x0 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3571        let mut x1 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3572        let mut x2 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3573        for ts in 0..num_ts {
3574            x0[ts][8] = (1.0, 0.0);
3575            x1[ts][12] = (0.0, 1.0);
3576            x2[ts][20] = (0.5, -0.5);
3577        }
3578        let alpha1 = vec![vec![0.3f32; num_pb as usize]];
3579        let beta1 = vec![vec![0.2f32; num_pb as usize]];
3580        let alpha2 = vec![vec![-0.4f32; num_pb as usize]];
3581        let beta2 = vec![vec![0.1f32; num_pb as usize]];
3582        let mut state = Acpl5xPairState::new();
3583        let frame = Acpl5xPairFrame {
3584            mode: Acpl5xPairMode::AspxAcpl2,
3585            x0: &x0,
3586            x1: &x1,
3587            x2: &x2,
3588            x3: None,
3589            x4: None,
3590            alpha_1_dq: &alpha1,
3591            beta_1_dq: &beta1,
3592            alpha_2_dq: &alpha2,
3593            beta_2_dq: &beta2,
3594            num_param_bands: num_pb,
3595            acpl_qmf_band: 0,
3596            steep_1: false,
3597            steep_2: false,
3598            param_timeslots_1: &[],
3599            param_timeslots_2: &[],
3600        };
3601        let out = run_pseudocode_117_5x(&mut state, frame);
3602        assert_eq!(out.z0.len(), num_ts);
3603        assert_eq!(out.z1.len(), num_ts);
3604        assert_eq!(out.z2.len(), num_ts);
3605        assert_eq!(out.z3.len(), num_ts);
3606        assert_eq!(out.z4.len(), num_ts);
3607        // Centre passthrough z4 == x2.
3608        for ts in 0..num_ts {
3609            for sb in 0..NUM_QMF_SUBBANDS {
3610                assert_eq!(out.z4[ts][sb], x2[ts][sb], "z4 != x2 at ts={ts} sb={sb}");
3611            }
3612        }
3613        // No NaN / Inf in any output sample.
3614        for ts in 0..num_ts {
3615            for sb in 0..NUM_QMF_SUBBANDS {
3616                for (re, im) in [
3617                    out.z0[ts][sb],
3618                    out.z1[ts][sb],
3619                    out.z2[ts][sb],
3620                    out.z3[ts][sb],
3621                    out.z4[ts][sb],
3622                ] {
3623                    assert!(re.is_finite(), "non-finite re at ts={ts} sb={sb}");
3624                    assert!(im.is_finite(), "non-finite im at ts={ts} sb={sb}");
3625                }
3626            }
3627        }
3628    }
3629
3630    /// In ASPX_ACPL_1 mode, the Ls / Rs carriers (`x3` / `x4`) are
3631    /// expected to participate in the M/S split below `acpl_qmf_band`.
3632    /// With alpha=beta=0 and `acpl_qmf_band=64` (whole-band M/S), the
3633    /// pair output should reduce to `(0.5*(x0+x3)*2, 0.5*(x0-x3)*2) =
3634    /// (x0+x3, x0-x3)` — note the inner `x0in = 2*x0` doubles the
3635    /// inputs first. We then have `z1 *= sqrt(2)`, so the surround
3636    /// channel is `(x0-x3) * sqrt(2)`.
3637    #[test]
3638    fn run_pseudocode_117_5x_aspx_acpl_1_low_band_ms_split() {
3639        let num_ts = 4usize;
3640        let num_pb = 9u32;
3641        let mut x0 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3642        let mut x1 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3643        let x2 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3644        let mut x3 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3645        let mut x4 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3646        // Pick distinct tones so z0 = (x0+x3) and z1 = (x0-x3)*sqrt(2).
3647        for ts in 0..num_ts {
3648            x0[ts][2] = (0.7, 0.0);
3649            x3[ts][2] = (0.3, 0.0);
3650            x1[ts][2] = (0.5, 0.5);
3651            x4[ts][2] = (0.1, -0.1);
3652        }
3653        let alpha1 = vec![vec![0.0f32; num_pb as usize]];
3654        let beta1 = vec![vec![0.0f32; num_pb as usize]];
3655        let alpha2 = vec![vec![0.0f32; num_pb as usize]];
3656        let beta2 = vec![vec![0.0f32; num_pb as usize]];
3657        let mut state = Acpl5xPairState::new();
3658        let frame = Acpl5xPairFrame {
3659            mode: Acpl5xPairMode::AspxAcpl1,
3660            x0: &x0,
3661            x1: &x1,
3662            x2: &x2,
3663            x3: Some(&x3),
3664            x4: Some(&x4),
3665            alpha_1_dq: &alpha1,
3666            beta_1_dq: &beta1,
3667            alpha_2_dq: &alpha2,
3668            beta_2_dq: &beta2,
3669            num_param_bands: num_pb,
3670            // Whole-band M/S split: every sb < 64 takes the (x0+x1)/2
3671            // and (x0-x1)/2 path inside acpl_module().
3672            acpl_qmf_band: NUM_QMF_SUBBANDS as u32,
3673            steep_1: false,
3674            steep_2: false,
3675            param_timeslots_1: &[],
3676            param_timeslots_2: &[],
3677        };
3678        let out = run_pseudocode_117_5x(&mut state, frame);
3679        let sq2 = (2.0f32).sqrt();
3680        for ts in 0..num_ts {
3681            // L-side: x0in = 2*x0, x3in = 2*x3 ->
3682            //   z0 = 0.5 * (x0in + x3in) = x0 + x3 = (1.0, 0.0)
3683            //   z1 = 0.5 * (x0in - x3in) = x0 - x3 = (0.4, 0.0); then *sqrt(2).
3684            let (z0r, z0i) = out.z0[ts][2];
3685            let (z1r, z1i) = out.z1[ts][2];
3686            assert!((z0r - 1.0).abs() < 1e-5, "ts={ts} z0r={z0r}");
3687            assert!((z0i - 0.0).abs() < 1e-5, "ts={ts} z0i={z0i}");
3688            assert!(
3689                (z1r - 0.4 * sq2).abs() < 1e-5,
3690                "ts={ts} z1r={z1r} expected={}",
3691                0.4 * sq2
3692            );
3693            assert!((z1i - 0.0).abs() < 1e-5, "ts={ts} z1i={z1i}");
3694            // R-side: x1in = 2*x1, x4in = 2*x4 ->
3695            //   z2 = 0.5 * (x1in + x4in) = x1 + x4 = (0.6, 0.4)
3696            //   z3 = 0.5 * (x1in - x4in) = x1 - x4 = (0.4, 0.6); then *sqrt(2).
3697            let (z2r, z2i) = out.z2[ts][2];
3698            let (z3r, z3i) = out.z3[ts][2];
3699            assert!((z2r - 0.6).abs() < 1e-5, "ts={ts} z2r={z2r}");
3700            assert!((z2i - 0.4).abs() < 1e-5, "ts={ts} z2i={z2i}");
3701            assert!(
3702                (z3r - 0.4 * sq2).abs() < 1e-5,
3703                "ts={ts} z3r={z3r} expected={}",
3704                0.4 * sq2
3705            );
3706            assert!(
3707                (z3i - 0.6 * sq2).abs() < 1e-5,
3708                "ts={ts} z3i={z3i} expected={}",
3709                0.6 * sq2
3710            );
3711        }
3712    }
3713
3714    /// Both modes: Pseudocode 117 must propagate decorrelator + ducker
3715    /// state across calls. After a frame, the L-side / R-side prev
3716    /// arrays are populated by the inner `run_pseudocode_115_pair()` —
3717    /// this test runs two frames and checks the second frame sees the
3718    /// updated `alpha_prev` / `beta_prev` from the first.
3719    #[test]
3720    fn run_pseudocode_117_5x_state_carries_across_frames() {
3721        let num_ts = 8usize;
3722        let num_pb = 9u32;
3723        let x0 = vec![[(1.0f32, 0.5f32); NUM_QMF_SUBBANDS]; num_ts];
3724        let x1 = vec![[(0.5f32, 1.0f32); NUM_QMF_SUBBANDS]; num_ts];
3725        let x2 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3726        let alpha1 = vec![vec![0.42f32; num_pb as usize]];
3727        let beta1 = vec![vec![0.13f32; num_pb as usize]];
3728        let alpha2 = vec![vec![-0.11f32; num_pb as usize]];
3729        let beta2 = vec![vec![0.21f32; num_pb as usize]];
3730        let mut state = Acpl5xPairState::new();
3731        // Pre-state: prev arrays are zeros.
3732        assert!(state.left_pair.alpha_prev_sb.iter().all(|&v| v == 0.0));
3733        assert!(state.right_pair.alpha_prev_sb.iter().all(|&v| v == 0.0));
3734        let frame = Acpl5xPairFrame {
3735            mode: Acpl5xPairMode::AspxAcpl2,
3736            x0: &x0,
3737            x1: &x1,
3738            x2: &x2,
3739            x3: None,
3740            x4: None,
3741            alpha_1_dq: &alpha1,
3742            beta_1_dq: &beta1,
3743            alpha_2_dq: &alpha2,
3744            beta_2_dq: &beta2,
3745            num_param_bands: num_pb,
3746            acpl_qmf_band: 0,
3747            steep_1: false,
3748            steep_2: false,
3749            param_timeslots_1: &[],
3750            param_timeslots_2: &[],
3751        };
3752        let _out1 = run_pseudocode_117_5x(&mut state, frame);
3753        // Post-state: prev arrays carry the last param-set's expanded
3754        // sb rows. With a single param set of constant 0.42 / -0.11
3755        // across all bands, every sb entry must equal that constant.
3756        for sb in 0..NUM_QMF_SUBBANDS {
3757            assert!(
3758                (state.left_pair.alpha_prev_sb[sb] - 0.42).abs() < 1e-6,
3759                "left alpha_prev[{sb}] = {}",
3760                state.left_pair.alpha_prev_sb[sb]
3761            );
3762            assert!(
3763                (state.right_pair.alpha_prev_sb[sb] - (-0.11)).abs() < 1e-6,
3764                "right alpha_prev[{sb}] = {}",
3765                state.right_pair.alpha_prev_sb[sb]
3766            );
3767            assert!(
3768                (state.left_pair.beta_prev_sb[sb] - 0.13).abs() < 1e-6,
3769                "left beta_prev[{sb}] = {}",
3770                state.left_pair.beta_prev_sb[sb]
3771            );
3772            assert!(
3773                (state.right_pair.beta_prev_sb[sb] - 0.21).abs() < 1e-6,
3774                "right beta_prev[{sb}] = {}",
3775                state.right_pair.beta_prev_sb[sb]
3776            );
3777        }
3778    }
3779
3780    /// ASPX_ACPL_2 (no surround carriers) should compute exactly the
3781    /// same per-side output as calling `run_pseudocode_115_pair()`
3782    /// twice with x1 = None — Pseudocode 117 is only adding the
3783    /// sqrt(2) post-scale + centre passthrough on top of two parallel
3784    /// channel-pair passes.
3785    #[test]
3786    fn run_pseudocode_117_5x_aspx_acpl_2_matches_two_115_passes() {
3787        let num_ts = 12usize;
3788        let num_pb = 9u32;
3789        let mut x0 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3790        let mut x1 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3791        let x2 = vec![[(0.0f32, 0.0f32); NUM_QMF_SUBBANDS]; num_ts];
3792        for ts in 0..num_ts {
3793            for sb in 0..32 {
3794                let phase = (ts as f32 * 0.2) + (sb as f32 * 0.1);
3795                x0[ts][sb] = (0.1 * phase.cos(), 0.1 * phase.sin());
3796                x1[ts][sb] = (0.05 * phase.sin(), 0.05 * phase.cos());
3797            }
3798        }
3799        let alpha1 = vec![vec![0.3f32; num_pb as usize]];
3800        let beta1 = vec![vec![0.2f32; num_pb as usize]];
3801        let alpha2 = vec![vec![0.1f32; num_pb as usize]];
3802        let beta2 = vec![vec![0.4f32; num_pb as usize]];
3803        // Reference: run two independent channel-pair passes.
3804        let mut ref_state_l = AcplCpeState::new(DecorrelatorId::D0);
3805        let mut ref_state_r = AcplCpeState::new(DecorrelatorId::D1);
3806        let ref_l = run_pseudocode_115_pair(
3807            &mut ref_state_l,
3808            AcplCpeFrame {
3809                x0: &x0,
3810                x1: None,
3811                alpha_dq: &alpha1,
3812                beta_dq: &beta1,
3813                num_param_bands: num_pb,
3814                acpl_qmf_band: 0,
3815                steep: false,
3816                param_timeslots: &[],
3817            },
3818        );
3819        let ref_r = run_pseudocode_115_pair(
3820            &mut ref_state_r,
3821            AcplCpeFrame {
3822                x0: &x1,
3823                x1: None,
3824                alpha_dq: &alpha2,
3825                beta_dq: &beta2,
3826                num_param_bands: num_pb,
3827                acpl_qmf_band: 0,
3828                steep: false,
3829                param_timeslots: &[],
3830            },
3831        );
3832        // Run Pseudocode 117 — should match the references modulo the
3833        // sqrt(2) post-scale on the surround pair.
3834        let mut state = Acpl5xPairState::new();
3835        let frame = Acpl5xPairFrame {
3836            mode: Acpl5xPairMode::AspxAcpl2,
3837            x0: &x0,
3838            x1: &x1,
3839            x2: &x2,
3840            x3: None,
3841            x4: None,
3842            alpha_1_dq: &alpha1,
3843            beta_1_dq: &beta1,
3844            alpha_2_dq: &alpha2,
3845            beta_2_dq: &beta2,
3846            num_param_bands: num_pb,
3847            acpl_qmf_band: 0,
3848            steep_1: false,
3849            steep_2: false,
3850            param_timeslots_1: &[],
3851            param_timeslots_2: &[],
3852        };
3853        let out = run_pseudocode_117_5x(&mut state, frame);
3854        let sq2 = (2.0f32).sqrt();
3855        // Spot-check a few subbands across the time axis.
3856        for ts in 0..num_ts {
3857            for sb in [0usize, 5, 16, 31, 47, 63] {
3858                let (a_r, a_i) = out.z0[ts][sb];
3859                let (e_r, e_i) = ref_l.z0[ts][sb];
3860                assert!(
3861                    (a_r - e_r).abs() < 1e-5 && (a_i - e_i).abs() < 1e-5,
3862                    "z0 mismatch ts={ts} sb={sb} got ({a_r},{a_i}) want ({e_r},{e_i})"
3863                );
3864                let (a_r, a_i) = out.z2[ts][sb];
3865                let (e_r, e_i) = ref_r.z0[ts][sb];
3866                assert!(
3867                    (a_r - e_r).abs() < 1e-5 && (a_i - e_i).abs() < 1e-5,
3868                    "z2 mismatch ts={ts} sb={sb} got ({a_r},{a_i}) want ({e_r},{e_i})"
3869                );
3870                let (a_r, a_i) = out.z1[ts][sb];
3871                let (e_r, e_i) = (ref_l.z1[ts][sb].0 * sq2, ref_l.z1[ts][sb].1 * sq2);
3872                assert!(
3873                    (a_r - e_r).abs() < 1e-5 && (a_i - e_i).abs() < 1e-5,
3874                    "z1 mismatch ts={ts} sb={sb} got ({a_r},{a_i}) want ({e_r},{e_i})"
3875                );
3876                let (a_r, a_i) = out.z3[ts][sb];
3877                let (e_r, e_i) = (ref_r.z1[ts][sb].0 * sq2, ref_r.z1[ts][sb].1 * sq2);
3878                assert!(
3879                    (a_r - e_r).abs() < 1e-5 && (a_i - e_i).abs() < 1e-5,
3880                    "z3 mismatch ts={ts} sb={sb} got ({a_r},{a_i}) want ({e_r},{e_i})"
3881                );
3882            }
3883        }
3884    }
3885
3886    // =====================================================================
3887    // §5.7.7.6 — 5_X-walker glue: PCM-level helpers for ASPX_ACPL_{1,2,3}
3888    // =====================================================================
3889
3890    /// `run_acpl_5x_pair_pcm` should reject inputs whose lengths don't
3891    /// align (one PCM channel a multiple of 64, all channels same
3892    /// length, surround presence consistent with mode).
3893    #[test]
3894    fn run_acpl_5x_pair_pcm_rejects_misaligned_inputs() {
3895        use crate::acpl::{
3896            AcplConfig1ch, AcplData1ch, AcplFramingData, AcplHuffParam, AcplInterpolationType,
3897            AcplQuantMode,
3898        };
3899        let cfg = AcplConfig1ch {
3900            num_param_bands_id: 0,
3901            num_param_bands: 15,
3902            quant_mode: AcplQuantMode::Fine,
3903            qmf_band: 0,
3904        };
3905        let data = AcplData1ch {
3906            framing: AcplFramingData {
3907                interpolation_type: AcplInterpolationType::Smooth,
3908                num_param_sets_cod: 0,
3909                num_param_sets: 1,
3910                param_timeslots: vec![],
3911            },
3912            alpha1: vec![AcplHuffParam {
3913                values: vec![4i32; cfg.num_param_bands as usize],
3914                direction_time: false,
3915            }],
3916            beta1: vec![AcplHuffParam {
3917                values: vec![2i32; cfg.num_param_bands as usize],
3918                direction_time: false,
3919            }],
3920        };
3921        let mut state = Acpl5xPairPcmState::new();
3922        // Length not a multiple of 64 → None.
3923        let pcm_short = vec![0.0f32; 65];
3924        assert!(run_acpl_5x_pair_pcm(
3925            Acpl5xPairMode::AspxAcpl2,
3926            &pcm_short,
3927            &pcm_short,
3928            &pcm_short,
3929            None,
3930            None,
3931            &cfg,
3932            &data,
3933            &cfg,
3934            &data,
3935            &mut state,
3936        )
3937        .is_none());
3938        // Surround present in ASPX_ACPL_2 mode → None.
3939        let pcm = vec![0.0f32; 64];
3940        assert!(run_acpl_5x_pair_pcm(
3941            Acpl5xPairMode::AspxAcpl2,
3942            &pcm,
3943            &pcm,
3944            &pcm,
3945            Some(&pcm),
3946            Some(&pcm),
3947            &cfg,
3948            &data,
3949            &cfg,
3950            &data,
3951            &mut state,
3952        )
3953        .is_none());
3954        // Surround missing in ASPX_ACPL_1 mode → None.
3955        assert!(run_acpl_5x_pair_pcm(
3956            Acpl5xPairMode::AspxAcpl1,
3957            &pcm,
3958            &pcm,
3959            &pcm,
3960            None,
3961            None,
3962            &cfg,
3963            &data,
3964            &cfg,
3965            &data,
3966            &mut state,
3967        )
3968        .is_none());
3969    }
3970
3971    /// End-to-end: feed three sine carriers (L/R/C) into the
3972    /// `ASPX_ACPL_2` 5_X PCM pipeline. We expect five output channels
3973    /// of the right length with all carrying energy and no NaN/Inf.
3974    #[test]
3975    fn run_acpl_5x_pair_pcm_aspx_acpl_2_emits_five_channels() {
3976        use crate::acpl::{
3977            AcplConfig1ch, AcplData1ch, AcplFramingData, AcplHuffParam, AcplInterpolationType,
3978            AcplQuantMode,
3979        };
3980        let n_slots = 64usize;
3981        let n = n_slots * NUM_QMF_SUBBANDS;
3982        let mut pcm_l = vec![0.0f32; n];
3983        let mut pcm_r = vec![0.0f32; n];
3984        let mut pcm_c = vec![0.0f32; n];
3985        let f_l = 440.0f32 / 48_000.0;
3986        let f_r = 220.0f32 / 48_000.0;
3987        let f_c = 880.0f32 / 48_000.0;
3988        for i in 0..n {
3989            pcm_l[i] = (2.0 * std::f32::consts::PI * f_l * i as f32).sin();
3990            pcm_r[i] = 0.5 * (2.0 * std::f32::consts::PI * f_r * i as f32).sin();
3991            pcm_c[i] = 0.3 * (2.0 * std::f32::consts::PI * f_c * i as f32).sin();
3992        }
3993        let cfg = AcplConfig1ch {
3994            num_param_bands_id: 0,
3995            num_param_bands: 15,
3996            quant_mode: AcplQuantMode::Fine,
3997            qmf_band: 0,
3998        };
3999        let data1 = AcplData1ch {
4000            framing: AcplFramingData {
4001                interpolation_type: AcplInterpolationType::Smooth,
4002                num_param_sets_cod: 0,
4003                num_param_sets: 1,
4004                param_timeslots: vec![],
4005            },
4006            alpha1: vec![AcplHuffParam {
4007                values: vec![4i32; cfg.num_param_bands as usize],
4008                direction_time: false,
4009            }],
4010            beta1: vec![AcplHuffParam {
4011                values: vec![2i32; cfg.num_param_bands as usize],
4012                direction_time: false,
4013            }],
4014        };
4015        let data2 = AcplData1ch {
4016            framing: AcplFramingData {
4017                interpolation_type: AcplInterpolationType::Smooth,
4018                num_param_sets_cod: 0,
4019                num_param_sets: 1,
4020                param_timeslots: vec![],
4021            },
4022            alpha1: vec![AcplHuffParam {
4023                values: vec![-3i32; cfg.num_param_bands as usize],
4024                direction_time: false,
4025            }],
4026            beta1: vec![AcplHuffParam {
4027                values: vec![1i32; cfg.num_param_bands as usize],
4028                direction_time: false,
4029            }],
4030        };
4031        let mut state = Acpl5xPairPcmState::new();
4032        let out = run_acpl_5x_pair_pcm(
4033            Acpl5xPairMode::AspxAcpl2,
4034            &pcm_l,
4035            &pcm_r,
4036            &pcm_c,
4037            None,
4038            None,
4039            &cfg,
4040            &data1,
4041            &cfg,
4042            &data2,
4043            &mut state,
4044        )
4045        .expect("synth runs");
4046        assert_eq!(out.left.len(), n);
4047        assert_eq!(out.right.len(), n);
4048        assert_eq!(out.centre.len(), n);
4049        assert_eq!(out.left_surround.len(), n);
4050        assert_eq!(out.right_surround.len(), n);
4051        let start = 2048usize;
4052        let energy =
4053            |b: &[f32]| -> f64 { b[start..].iter().map(|&s| (s as f64).powi(2)).sum::<f64>() };
4054        assert!(energy(&out.left) > 1e-6, "left silent");
4055        assert!(energy(&out.right) > 1e-6, "right silent");
4056        assert!(energy(&out.centre) > 1e-6, "centre silent");
4057        assert!(energy(&out.left_surround) > 1e-6, "Ls silent");
4058        assert!(energy(&out.right_surround) > 1e-6, "Rs silent");
4059        for b in [
4060            &out.left,
4061            &out.right,
4062            &out.centre,
4063            &out.left_surround,
4064            &out.right_surround,
4065        ] {
4066            for &s in b.iter() {
4067                assert!(s.is_finite(), "non-finite sample");
4068            }
4069        }
4070    }
4071
4072    /// `run_acpl_5x_mch_pcm` should reject misaligned inputs and emit
4073    /// five distinct PCM streams when fed a real ASPX_ACPL_3 frame.
4074    #[test]
4075    fn run_acpl_5x_mch_pcm_aspx_acpl_3_emits_five_channels() {
4076        use crate::acpl::{
4077            AcplConfig2ch, AcplData2ch, AcplFramingData, AcplHuffParam, AcplInterpolationType,
4078            AcplQuantMode,
4079        };
4080        let n_slots = 64usize;
4081        let n = n_slots * NUM_QMF_SUBBANDS;
4082        let mut pcm_l = vec![0.0f32; n];
4083        let mut pcm_r = vec![0.0f32; n];
4084        let mut pcm_c = vec![0.0f32; n];
4085        let f_l = 440.0f32 / 48_000.0;
4086        let f_r = 220.0f32 / 48_000.0;
4087        let f_c = 660.0f32 / 48_000.0;
4088        for i in 0..n {
4089            pcm_l[i] = (2.0 * std::f32::consts::PI * f_l * i as f32).sin();
4090            pcm_r[i] = (2.0 * std::f32::consts::PI * f_r * i as f32).cos();
4091            pcm_c[i] = 0.3 * (2.0 * std::f32::consts::PI * f_c * i as f32).sin();
4092        }
4093        let cfg = AcplConfig2ch {
4094            num_param_bands_id: 0,
4095            num_param_bands: 15,
4096            quant_mode_0: AcplQuantMode::Fine,
4097            quant_mode_1: AcplQuantMode::Fine,
4098        };
4099        let mk_huff = |seed: i32| AcplHuffParam {
4100            values: vec![seed; cfg.num_param_bands as usize],
4101            direction_time: false,
4102        };
4103        let data = AcplData2ch {
4104            framing: AcplFramingData {
4105                interpolation_type: AcplInterpolationType::Smooth,
4106                num_param_sets_cod: 0,
4107                num_param_sets: 1,
4108                param_timeslots: vec![],
4109            },
4110            alpha1: vec![mk_huff(4)],
4111            alpha2: vec![mk_huff(-3)],
4112            beta1: vec![mk_huff(2)],
4113            beta2: vec![mk_huff(1)],
4114            beta3: vec![mk_huff(0)],
4115            gamma1: vec![mk_huff(2)],
4116            gamma2: vec![mk_huff(0)],
4117            gamma3: vec![mk_huff(0)],
4118            gamma4: vec![mk_huff(2)],
4119            gamma5: vec![mk_huff(1)],
4120            gamma6: vec![mk_huff(1)],
4121        };
4122        let mut state = Acpl5xMchPcmState::new();
4123        // Reject mismatched lengths.
4124        let pcm_bad = vec![0.0f32; 65];
4125        assert!(
4126            run_acpl_5x_mch_pcm(&pcm_bad, &pcm_bad, &pcm_bad, &cfg, &data, &mut state).is_none()
4127        );
4128        // Run end-to-end on the well-formed frame.
4129        let out = run_acpl_5x_mch_pcm(&pcm_l, &pcm_r, &pcm_c, &cfg, &data, &mut state)
4130            .expect("synth runs");
4131        assert_eq!(out.left.len(), n);
4132        assert_eq!(out.right.len(), n);
4133        assert_eq!(out.centre.len(), n);
4134        assert_eq!(out.left_surround.len(), n);
4135        assert_eq!(out.right_surround.len(), n);
4136        let start = 2048usize;
4137        let energy =
4138            |b: &[f32]| -> f64 { b[start..].iter().map(|&s| (s as f64).powi(2)).sum::<f64>() };
4139        for b in [
4140            &out.left,
4141            &out.right,
4142            &out.centre,
4143            &out.left_surround,
4144            &out.right_surround,
4145        ] {
4146            for &s in b.iter() {
4147                assert!(s.is_finite(), "non-finite sample");
4148            }
4149        }
4150        assert!(energy(&out.left) > 1e-6, "left silent");
4151        assert!(energy(&out.right) > 1e-6, "right silent");
4152        assert!(energy(&out.centre) > 1e-6, "centre silent");
4153    }
4154}