Skip to main content

oxideav_ac4/
encoder_ims.rs

1//! AC-4 IMS (Immersive Multichannel Service) encoder scaffold.
2//!
3//! Round 46 — Auditor-mode scaffold for the IMS encoder per ETSI
4//! TS 103 190-2 V1.2.1 §6.2 / §6.3.2.1 (`ac4_toc()`). Emits a
5//! structurally-valid `raw_ac4_frame()` with an IMS-flavoured TOC
6//! (`bitstream_version = 2` + `ac4_presentation_v1_info()` +
7//! `ac4_substream_group_info()`); the substream body itself is
8//! all-zero placeholder bits — the encoder side of the audio pipeline
9//! (MDCT analysis, scalefactor selection, ASF/SSF entropy coding, A-SPX
10//! envelope coding, A-CPL parameter extraction) is deferred. The
11//! decoder-side counterpart is expected to re-tile zeros back to
12//! silence PCM.
13//!
14//! Round 47 fixes the v2 TOC bit layout to match the literal §6.2.1.1
15//! / §6.2.1.3 / §6.3.2.5 syntax boxes (the round-46 scaffold skipped
16//! `b_hsf_ext` / `b_single_substream` in `ac4_substream_group_info()`
17//! and emitted a stale `ac4_presentation_v1_info()` skeleton missing
18//! `mdcompat`, `frame_rate_fractions_info()`, `emdf_info()`,
19//! `b_presentation_filter`, and the trailing
20//! `ac4_substream_info_chan()` body). The matching v2 dispatch in
21//! [`crate::toc::parse_ac4_toc`] now walks the same syntax, so v2
22//! `Ac4ImsEncoder::encode_frame()` → `parse_ac4_toc` round-trips the
23//! same `(channels, samples, sample_rate, b_iframe_global)` tuple as
24//! the v0 path.
25//!
26//! The Auditor-mode goal is to land the public type surface and the
27//! TOC writer so downstream tooling (TS 103 190-2 conformance
28//! checkers, MP4 packagers, demux smoke tests) can pull a real frame
29//! through the round-trip. Production-grade IMS encoding is multiple
30//! weeks of work.
31//!
32//! ## What this scaffold does
33//!
34//! * Emits an IMS `ac4_toc()` frame header per §6.2.1.1 syntax box:
35//!   `bitstream_version` (2 b) + `sequence_counter` (10 b) +
36//!   `b_wait_frames` (1 b) + `fs_index` (1 b) + `frame_rate_index`
37//!   (4 b) + `b_iframe_global` (1 b) + `b_single_presentation` (1 b) +
38//!   `b_payload_base` (1 b). For `bitstream_version == 2` the per-pres
39//!   loop calls `ac4_presentation_v1_info()` (§6.2.1.3) followed by
40//!   the `ac4_substream_group_info()` element (§6.3.2.5). The encoder
41//!   produces a single-presentation, single-substream-group frame —
42//!   the smallest IMS shape that round-trips through a demuxer.
43//!
44//! * Provides a TS 103 190-1 fallback ([`Ac4ImsEncoder::encode_frame_v0`])
45//!   that emits a `bitstream_version == 0` TOC. The decoder in this
46//!   crate (which currently parses the v0 syntax — the v1 / v2 variant
47//!   from TS 103 190-2 is an orthogonal future round) accepts this
48//!   path and yields a structurally-valid silent `AudioFrame` of the
49//!   declared duration. The IMS-flavoured `encode_frame` itself is
50//!   round-trip-validated against a forward `parse_ac4_toc` call to
51//!   confirm the header bytes describe the same `(channels, samples,
52//!   sample_rate, b_iframe_global)` tuple back to the caller — even
53//!   though the `bitstream_version == 2` branch of `parse_ac4_toc`
54//!   itself isn't yet implemented.
55//!
56//! ## What this scaffold does NOT do
57//!
58//! * No MDCT analysis. The audio body is emitted as zero bits so the
59//!   `audio_size_value` field is honest about an empty payload.
60//! * No A-SPX envelope coding, no A-CPL parameter extraction, no
61//!   metadata (DRC / DE / EMDF) emission — those are all silent /
62//!   absent in the produced frame.
63//! * No `ac4_substream_group_info()` body beyond the
64//!   `b_substreams_present == 1` + `n_lf_substreams == 2` skeleton.
65//!   The `sus_ver` bit + the per-substream `b_audio_ndot` /
66//!   `b_pres_ndot` / `b_oamd_ndot` flags are all zero.
67//! * No bit-rate signalling beyond `br_code = 0`.
68
69use oxideav_core::bits::BitWriter;
70
71use crate::encoder_asf::{
72    average_per_sfb_correlation, build_5_0_simple_asf_body_from_pcm_spectra,
73    build_5_1_simple_asf_body_from_pcm_spectra, build_7_0_simple_asf_body_from_pcm_spectra,
74    build_7_1_simple_asf_body_from_pcm_spectra, build_mono_simple_asf_body_from_pcm_spectrum,
75    build_stereo_simple_asf_joint_body_from_pcm_spectra,
76    build_stereo_simple_asf_split_body_from_pcm_spectra,
77};
78use crate::encoder_mdct::EncoderMdctState;
79
80/// Encoder-side builder for AC-4 IMS frames. One instance per audio
81/// stream — carries the 10-bit `sequence_counter` rolling counter and
82/// the canonical frame layout (sample rate, frame-rate index, channel
83/// mode) so each `encode_frame()` call produces a structurally-valid
84/// output frame ready to wrap in a sync-frame (`0xAC40` / `0xAC41`)
85/// or hand to an MP4 muxer.
86///
87/// Round 46 lands the Auditor-mode bit layout per ETSI TS 103 190-2
88/// §6.2.1.1 — the audio body itself is all-zero placeholder bits.
89#[derive(Debug, Clone)]
90pub struct Ac4ImsEncoder {
91    /// `bitstream_version` value to emit (TS 103 190-2 Table 74).
92    /// `0` selects the TS 103 190-1 v0 path (`ac4_presentation_info()`
93    /// per-pres); `2` selects the IMS path
94    /// (`ac4_presentation_v1_info()` + `ac4_substream_group_info()`).
95    pub bitstream_version: u8,
96    /// Rolling 10-bit `sequence_counter` field — wraps modulo 1024.
97    pub sequence_counter: u16,
98    /// `fs_index` (1 b): 0 → 44.1 kHz, 1 → 48 kHz.
99    pub fs_index: u8,
100    /// `frame_rate_index` (4 b) per Table 83 / 84.
101    pub frame_rate_index: u8,
102    /// `b_iframe_global` flag for this frame.
103    pub b_iframe_global: bool,
104    /// Channel mode prefix code per Table 85 (TS 103 190-1) / Table
105    /// 78 (TS 103 190-2): `0b0` → mono, `0b10` → stereo, etc.
106    /// Encoded as the literal prefix in the low-order bits of
107    /// `channel_mode_value` with the bit count in
108    /// `channel_mode_bits`.
109    pub channel_mode_value: u8,
110    /// Bit-width of `channel_mode_value` (1..=11).
111    pub channel_mode_bits: u8,
112    /// Forward-MDCT analysis state for `encode_frame_pcm()`. Carries
113    /// the previous frame's `N` PCM samples so the 50% TDAC overlap
114    /// runs correctly across frames. Lazy-initialised on first use.
115    pub mdct_state: Option<EncoderMdctState>,
116    /// Forward-MDCT analysis state for the secondary (right) channel of
117    /// `encode_frame_pcm_stereo()`. Identical role to `mdct_state` but
118    /// for the second channel — separate so 50% TDAC overlap is
119    /// per-channel.
120    pub mdct_state_r: Option<EncoderMdctState>,
121    /// Forward-MDCT analysis state for the multichannel encoder paths
122    /// (`encode_frame_pcm_5_0()` and any future N>2 variants). One
123    /// [`EncoderMdctState`] per output channel — separate so 50% TDAC
124    /// overlap continuity is preserved per channel across frames. Lazy-
125    /// initialised on first use; grown to the required channel count.
126    pub mdct_states_multi: Vec<EncoderMdctState>,
127}
128
129impl Ac4ImsEncoder {
130    /// New encoder defaulting to the smallest-valid IMS shape:
131    /// `bitstream_version = 2`, sequence_counter = 0, 48 kHz, 24 fps
132    /// (`frame_rate_index = 1`), b_iframe_global = 1, mono channel
133    /// mode (`0b0`, 1 b).
134    pub fn new() -> Self {
135        Self {
136            bitstream_version: 2,
137            sequence_counter: 0,
138            fs_index: 1,
139            frame_rate_index: 1,
140            b_iframe_global: true,
141            channel_mode_value: 0b0,
142            channel_mode_bits: 1,
143            mdct_state: None,
144            mdct_state_r: None,
145            mdct_states_multi: Vec::new(),
146        }
147    }
148
149    /// Switch to a TS 103 190-1 v0 frame layout. The decoder in this
150    /// crate parses v0 today; v2 is structurally emitted but not yet
151    /// re-parsed end-to-end.
152    pub fn with_v0(mut self) -> Self {
153        self.bitstream_version = 0;
154        self
155    }
156
157    /// Stereo channel mode (`0b10`, 2 b).
158    pub fn with_stereo(mut self) -> Self {
159        self.channel_mode_value = 0b10;
160        self.channel_mode_bits = 2;
161        self
162    }
163
164    /// 5.0 channel mode (`0b1101`, 4 b) per Table 85 — channel_mode 3 —
165    /// the 5.0 surround layout (`L, R, C, Ls, Rs`) without LFE. Drives the
166    /// decoder's `5_X_channel_element()` walker for `channels == 5` (no
167    /// `b_has_lfe` block) and the corresponding `dispatch_5x_cfg3_simple_aspx`
168    /// PCM output path.
169    pub fn with_5_0(mut self) -> Self {
170        self.channel_mode_value = 0b1101;
171        self.channel_mode_bits = 4;
172        self
173    }
174
175    /// 5.1 channel mode (`0b1110`, 4 b) per Table 85.
176    pub fn with_5_1(mut self) -> Self {
177        self.channel_mode_value = 0b1110;
178        self.channel_mode_bits = 4;
179        self
180    }
181
182    /// 7.0 (3/4/0) channel mode (`0b1111000`, 7 b) per ETSI TS 103 190-1
183    /// §4.3.3.7.1 Table 88 — channel_mode value `1111000` → ch_mode 5 → 7
184    /// channels with layout `L, C, R, Ls, Rs, Lb, Rb`. Drives the decoder's
185    /// `7_X_channel_element()` walker for `channels == 7` (no `b_has_lfe`
186    /// — that branch is gated on channel_mode 6 / 7.1) per §4.2.6.14
187    /// Table 33. The decoder's internal coding order for the inner
188    /// `five_channel_data()` is `[L, R, C, Ls, Rs]` per Table 180 (the
189    /// inner SCE order differs from the surface Table 88 listing's `L, C,
190    /// R` ordering — the decoder treats the inner five_channel_data slots
191    /// as L/R/C/Ls/Rs).
192    pub fn with_7_0(mut self) -> Self {
193        self.channel_mode_value = 0b1111000;
194        self.channel_mode_bits = 7;
195        self
196    }
197
198    /// 7.1 (3/4/0.1) channel mode (`0b1111001`, 7 b) per ETSI TS 103 190-1
199    /// §4.3.3.7.1 Table 88 — channel_mode value `1111001` → ch_mode 6 → 8
200    /// channels with layout `L, C, R, Ls, Rs, Lb, Rb, LFE`. Drives the
201    /// decoder's `7_X_channel_element()` walker for `channels == 8` (with
202    /// `b_has_lfe`) per §4.2.6.14 Table 33. The decoder's internal coding
203    /// order for the inner `five_channel_data()` is `[L, R, C, Ls, Rs]`
204    /// per Table 180 (the inner SCE order differs from the surface
205    /// Table 88 listing's `L, C, R` ordering — the decoder treats the
206    /// inner five_channel_data slots as L/R/C/Ls/Rs).
207    pub fn with_7_1(mut self) -> Self {
208        self.channel_mode_value = 0b1111001;
209        self.channel_mode_bits = 7;
210        self
211    }
212
213    /// Encode one Auditor-mode frame: emits a `raw_ac4_frame()`
214    /// payload (TOC + minimum-viable substream skeleton) and bumps
215    /// `sequence_counter`. Returns the produced bytes.
216    pub fn encode_frame(&mut self, body_padding_bytes: usize) -> Vec<u8> {
217        let mut bw = BitWriter::new();
218        self.write_toc(&mut bw);
219        bw.align_to_byte();
220        let mut frame = bw.finish();
221        // Pad the substream body with zeros so downstream demuxers see
222        // a non-empty frame. `body_padding_bytes` lets callers tune
223        // the final frame size for size-table tests.
224        if body_padding_bytes > 0 {
225            frame.extend(vec![0u8; body_padding_bytes]);
226        }
227        // sequence_counter is 10 bits — wrap modulo 1024.
228        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
229        frame
230    }
231
232    /// Encode the same frame at `bitstream_version = 0` regardless of
233    /// the encoder's configured version — used by the round-trip test
234    /// to feed a TS 103 190-1-decodable frame back through
235    /// [`crate::toc::parse_ac4_toc`].
236    pub fn encode_frame_v0(&mut self, body_padding_bytes: usize) -> Vec<u8> {
237        let saved = self.bitstream_version;
238        self.bitstream_version = 0;
239        let f = self.encode_frame(body_padding_bytes);
240        self.bitstream_version = saved;
241        f
242    }
243
244    /// Emit the `ac4_toc()` element per ETSI TS 103 190-2 §6.2.1.1.
245    /// The leading shared-with-v0 prefix is identical
246    /// (bitstream_version + sequence_counter + b_wait_frames +
247    /// fs_index + frame_rate_index + b_iframe_global +
248    /// b_single_presentation + b_payload_base + per-presentation
249    /// loop). For `bitstream_version <= 1` the per-pres loop runs
250    /// `ac4_presentation_info()` (TS 103 190-1 Table 5); for
251    /// `bitstream_version >= 2` it runs `ac4_presentation_v1_info()`
252    /// (TS 103 190-2 §6.2.1.3) then the per-substream-group
253    /// `ac4_substream_group_info()` (§6.3.2.5).
254    fn write_toc(&self, bw: &mut BitWriter) {
255        // bitstream_version (2 b) — Table 74.
256        bw.write_u32(self.bitstream_version as u32, 2);
257        // sequence_counter (10 b).
258        bw.write_u32(self.sequence_counter as u32, 10);
259        // b_wait_frames = 0.
260        bw.write_u32(0, 1);
261        // fs_index (1 b), frame_rate_index (4 b).
262        bw.write_u32(self.fs_index as u32, 1);
263        bw.write_u32(self.frame_rate_index as u32, 4);
264        // b_iframe_global, b_single_presentation = 1.
265        bw.write_u32(if self.b_iframe_global { 1 } else { 0 }, 1);
266        bw.write_u32(1, 1);
267        // b_payload_base = 0.
268        bw.write_u32(0, 1);
269
270        if self.bitstream_version <= 1 {
271            self.write_presentation_v0(bw);
272        } else {
273            // TS 103 190-2 §6.2.1.1: for bitstream_version > 1 the TOC
274            // carries a single `b_program_id` flag (no short_program_id /
275            // program_uuid in this scaffold), then the per-pres
276            // `ac4_presentation_v1_info()` loop, then the per-group
277            // `ac4_substream_group_info()` loop. Round 47 emits a
278            // single-presentation, single-substream-group frame: the
279            // smallest IMS shape that round-trips through `parse_ac4_toc`.
280            bw.write_u32(0, 1); // b_program_id = 0 (no program identifier)
281            self.write_presentation_v1_info(bw);
282            self.write_substream_group_info(bw);
283        }
284        // substream_index_table(): n_substreams = 1, b_size_present = 0
285        // (single-substream layout).
286        bw.write_u32(1, 2);
287        bw.write_u32(0, 1);
288    }
289
290    /// `ac4_presentation_info()` per ETSI TS 103 190-1 §4.3.3.3
291    /// (Table 5) — single-substream form for the `bitstream_version
292    /// <= 1` path. Mirrors the existing `build_mono_toc()` /
293    /// `build_minimal_toc()` test helpers in `decoder.rs` so
294    /// `parse_ac4_toc` accepts the produced frame end-to-end.
295    fn write_presentation_v0(&self, bw: &mut BitWriter) {
296        // ac4_presentation_info():
297        bw.write_u32(1, 1); // b_single_substream
298        bw.write_u32(0, 1); // presentation_version = 0
299        bw.write_u32(0, 3); // md_compat
300        bw.write_u32(0, 1); // b_belongs_to_presentation_id
301        bw.write_u32(0, 1); // frame_rate_multiply_info bit
302                            // emdf_info():
303        bw.write_u32(0, 2); // emdf_version
304        bw.write_u32(0, 3); // key_id
305        bw.write_u32(0, 1); // b_emdf_payloads_substream_info
306        bw.write_u32(0, 1); // emdf_reserved.b_more
307                            // ac4_substream_info():
308        bw.write_u32(
309            self.channel_mode_value as u32,
310            self.channel_mode_bits as u32,
311        );
312        bw.write_u32(0, 1); // b_sf_multiplier
313        bw.write_u32(0, 1); // b_bitrate_info
314        bw.write_u32(0, 1); // b_content_type
315        bw.write_u32(1, 1); // b_iframe
316        bw.write_u32(0, 2); // substream_index
317        bw.write_u32(0, 1); // b_pre_virtualized
318        bw.write_u32(0, 1); // b_add_emdf_substreams
319    }
320
321    /// `ac4_presentation_v1_info()` per ETSI TS 103 190-2 §6.2.1.3 —
322    /// single-substream-group form for `bitstream_version >= 2`:
323    /// `b_single_substream_group = 1`, then `presentation_version() = 0`
324    /// (single zero-bit since `bitstream_version != 1`), `mdcompat = 0`,
325    /// `b_presentation_id = 0`, `frame_rate_multiply_info()` (one bit
326    /// for `frame_rate_index = 1`), `frame_rate_fractions_info()`
327    /// (zero bits for index 1), `emdf_info()` (minimum form),
328    /// `b_presentation_filter = 0`, `ac4_sgi_specifier()` referencing
329    /// `group_index = 0`, `b_pre_virtualized = 0`,
330    /// `b_add_emdf_substreams = 0`, and `ac4_presentation_substream_info()`
331    /// (b_alternative = 0, b_pres_ndot = !iframe, substream_index = 0).
332    fn write_presentation_v1_info(&self, bw: &mut BitWriter) {
333        // b_single_substream_group = 1.
334        bw.write_u32(1, 1);
335        // presentation_version() = 0 — single '0' bit (loop terminates
336        // immediately). Emitted for bitstream_version != 1.
337        bw.write_u32(0, 1);
338        // mdcompat = 0 (3 b) — emitted for bitstream_version != 1.
339        bw.write_u32(0, 3);
340        // b_presentation_id = 0.
341        bw.write_u32(0, 1);
342        // frame_rate_multiply_info(): single b_multiplier bit for
343        // frame_rate_index in {0, 1, 7, 8, 9}.
344        bw.write_u32(0, 1);
345        // frame_rate_fractions_info(): nothing for frame_rate_index < 5
346        // or > 12.
347        // emdf_info(): emdf_version=0 (2b), key_id=0 (3b),
348        //   b_emdf_payloads_substream_info=0, emdf_reserved.b_more=0.
349        bw.write_u32(0, 2);
350        bw.write_u32(0, 3);
351        bw.write_u32(0, 1);
352        bw.write_u32(0, 1);
353        // b_presentation_filter = 0.
354        bw.write_u32(0, 1);
355        // ac4_sgi_specifier(): group_index = 0 (3 b, no variable_bits
356        // extension since group_index < 7).
357        bw.write_u32(0, 3);
358        // b_pre_virtualized = 0, b_add_emdf_substreams = 0.
359        bw.write_u32(0, 1);
360        bw.write_u32(0, 1);
361        // ac4_presentation_substream_info(): b_alternative = 0,
362        // b_pres_ndot = !b_iframe_global, substream_index = 0 (2 b).
363        bw.write_u32(0, 1);
364        bw.write_u32(if self.b_iframe_global { 0 } else { 1 }, 1);
365        bw.write_u32(0, 2);
366    }
367
368    /// `ac4_substream_group_info()` per ETSI TS 103 190-2 §6.3.2.5 —
369    /// single channel-coded substream skeleton matching the encoder's
370    /// `n_substreams = 1` substream_index_table.
371    fn write_substream_group_info(&self, bw: &mut BitWriter) {
372        // b_substreams_present = 1.
373        bw.write_u32(1, 1);
374        // b_hsf_ext = 0 — no high-sample-rate extension.
375        bw.write_u32(0, 1);
376        // b_single_substream = 1 — n_lf_substreams = 1.
377        bw.write_u32(1, 1);
378        // b_channel_coded = 1 — channel-based audio (vs object).
379        bw.write_u32(1, 1);
380        // ac4_substream_info_chan(b_substreams_present = 1):
381        //   channel_mode = encoder field (1..7 b),
382        //   fs_index == 1: b_sf_multiplier = 0,
383        //   b_bitrate_info = 0,
384        //   frame_rate_factor copies of b_audio_ndot = !iframe,
385        //   substream_index = 0 (2 b, since b_substreams_present = 1).
386        bw.write_u32(
387            self.channel_mode_value as u32,
388            self.channel_mode_bits as u32,
389        );
390        if self.fs_index == 1 {
391            bw.write_u32(0, 1); // b_sf_multiplier
392        }
393        bw.write_u32(0, 1); // b_bitrate_info
394                            // frame_rate_factor for {0,1,7,8,9} with
395                            // b_multiplier=0 is 1; for {2,3,4} also 1; otherwise 1.
396                            // → 1 b_audio_ndot bit.
397        bw.write_u32(if self.b_iframe_global { 0 } else { 1 }, 1);
398        bw.write_u32(0, 2); // substream_index
399                            // b_content_type = 0.
400        bw.write_u32(0, 1);
401    }
402}
403
404impl Default for Ac4ImsEncoder {
405    fn default() -> Self {
406        Self::new()
407    }
408}
409
410/// Build a mono SIMPLE/ASF `ac4_substream()` body that injects a single
411/// quantised spectral line at the specified scale-factor band. The
412/// payload is sized for `transform_length = 1920` (24 fps @ 48 kHz)
413/// with `max_sfb = 10`, matching the encoder's default frame layout.
414///
415/// `tone_cb_idx` selects the HCB5 codeword for the first spectral pair
416/// — `49` (q0=+1, q1=0) is the simplest signal-bearing choice. The
417/// remaining pairs all use codeword `40` (q0=0, q1=0). Reference scale
418/// factor is 120 (`sf_gain = 32.0`).
419///
420/// The returned bytes are the substream body (no TOC) that should be
421/// concatenated after the byte-aligned `ac4_toc()` element. They are
422/// padded to `pad_target_bytes` bytes with zeros so the
423/// `audio_size_value` field in the header matches the actual payload
424/// length.
425///
426/// Per ETSI TS 103 190-1 §5.7 (SIMPLE mode) + §5.8 (ASF). The full
427/// closed-form encoder for arbitrary input PCM (MDCT analysis +
428/// scalefactor selection + entropy coding) is deferred — round 47
429/// ships the canned-tone path so the encoder can produce non-silent
430/// PCM end-to-end.
431pub fn build_mono_simple_asf_tone_body(
432    transform_length: u32,
433    max_sfb: u32,
434    tone_cb_idx: usize,
435    tone_pair_idx: u32,
436    pad_target_bytes: usize,
437) -> Vec<u8> {
438    let mut bw = BitWriter::new();
439    // ac4_substream() per §5.7.1: audio_size_value (15 b) + b_more_bits
440    // (1 b). We declare the announced size as the pad target so the
441    // outer demuxer reads the entire padded body. b_more_bits = 0 so
442    // the 15-bit field is taken literally.
443    let audio_size = pad_target_bytes as u32;
444    let audio_size_lo = audio_size & 0x7FFF;
445    bw.write_u32(audio_size_lo, 15);
446    bw.write_bit(false);
447    bw.align_to_byte();
448    // audio_data() for channel_mode = 0 (mono), b_iframe = 1:
449    //   mono_codec_mode = 0 (SIMPLE), spec_frontend = 0 (ASF),
450    //   asf_transform_info() with b_long_frame = 1,
451    //   asf_psy_info(0, 0) with max_sfb[0] in 6 bits.
452    bw.write_u32(0, 1); // mono_codec_mode = SIMPLE
453    bw.write_u32(0, 1); // spec_frontend = ASF
454    bw.write_bit(true); // b_long_frame = 1
455    bw.write_u32(max_sfb, 6); // max_sfb[0]
456                              // asf_section_data: one section covering 0..max_sfb with cb=5
457                              // (HCB5, dim=2, signed). n_sect_bits = 3 (transf_length_idx=0
458                              // for long frame).
459    bw.write_u32(5, 4); // sect_cb
460    write_sect_len_incr(&mut bw, max_sfb, 3, 7);
461    // asf_spectral_data: emit `tone_cb_idx` for pair `tone_pair_idx`,
462    // and codeword 40 (q0=0, q1=0) for every other pair.
463    let sfbo = crate::sfb_offset::sfb_offset_48(transform_length).expect("invalid tl");
464    let end_line = sfbo[max_sfb as usize] as u32;
465    let hcb = crate::huffman::asf_hcb(5u32).expect("HCB5 must exist");
466    let pairs = end_line / 2;
467    let zero_cw = hcb.cw[40];
468    let zero_len = hcb.len[40] as u32;
469    let tone_cw = hcb.cw[tone_cb_idx];
470    let tone_len = hcb.len[tone_cb_idx] as u32;
471    for p in 0..pairs {
472        if p == tone_pair_idx {
473            bw.write_u32(tone_cw, tone_len);
474        } else {
475            bw.write_u32(zero_cw, zero_len);
476        }
477    }
478    // asf_scalefac_data: reference_scale_factor = 120 → sf_gain = 32.0.
479    bw.write_u32(120, 8);
480    // asf_snf_data: b_snf_data_exists = 0.
481    bw.write_u32(0, 1);
482    bw.align_to_byte();
483    while bw.byte_len() < pad_target_bytes {
484        bw.write_u32(0, 8);
485    }
486    bw.finish()
487}
488
489/// Write a section-length increment sequence per §4.3.5.4
490/// (Pseudocode 17). For `n_sect_bits = 3`, escape value 7,
491/// `sect_len = 1 + 7k + incr`: emit `k` escape codes followed by one
492/// non-escape `incr` (0..6).
493fn write_sect_len_incr(bw: &mut BitWriter, sect_len: u32, n_sect_bits: u32, esc: u32) {
494    let base = sect_len.saturating_sub(1);
495    let k = base / esc;
496    let incr = base % esc;
497    for _ in 0..k {
498        bw.write_u32(esc, n_sect_bits);
499    }
500    bw.write_u32(incr, n_sect_bits);
501}
502
503impl Ac4ImsEncoder {
504    /// Encode one IMS v2 frame containing a mono SIMPLE/ASF audio
505    /// substream that injects a single quantised spectral tone (per
506    /// `tone_cb_idx` from the ETSI Annex A HCB5 codebook). The decoder
507    /// dequantises the tone via `rec_spec = sign(q)|q|^(4/3)` and the
508    /// IMDCT + KBD windowing produce real, non-silent PCM.
509    ///
510    /// This is the canned-tone closed-form encoder mentioned in round-47
511    /// scope: full MDCT analysis + scalefactor optimisation + ASF
512    /// entropy coding for arbitrary PCM input is deferred. The shape
513    /// of this method (input PCM → bytes) is reserved for that future
514    /// work; for now it ignores its `_input_pcm` argument and emits
515    /// the canned tone payload.
516    ///
517    /// Per ETSI TS 103 190-1 §5.7 + §5.8.
518    pub fn encode_frame_mono_tone(&mut self, tone_cb_idx: usize, tone_pair_idx: u32) -> Vec<u8> {
519        // Force mono channel_mode for the tone helper — the canned ASF
520        // body is mono SIMPLE only.
521        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
522        self.channel_mode_value = 0b0;
523        self.channel_mode_bits = 1;
524        let mut bw = BitWriter::new();
525        self.write_toc(&mut bw);
526        bw.align_to_byte();
527        let mut frame = bw.finish();
528        // Append the canned-tone substream body. Size matches the test
529        // helpers in `decoder.rs` (420 bytes) so the substream parser
530        // sees a complete payload.
531        let body = build_mono_simple_asf_tone_body(1920, 10, tone_cb_idx, tone_pair_idx, 420);
532        frame.extend(body);
533        // sequence_counter wraps at 1024.
534        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
535        self.channel_mode_value = saved_mode.0;
536        self.channel_mode_bits = saved_mode.1;
537        frame
538    }
539
540    /// Encode one IMS v2 mono frame from arbitrary float PCM input
541    /// (range `[-1.0, 1.0]`). Returns the produced frame bytes.
542    ///
543    /// Pipeline (round 48):
544    ///   1. Forward MDCT analysis with KBD windowing across the 50% TDAC
545    ///      boundary (carries prior-frame `N` samples in the per-encoder
546    ///      [`EncoderMdctState`]).
547    ///   2. Per-band scalefactor selection (greedy nearest power-of-two
548    ///      that keeps |q| within the chosen Huffman codebook's bound).
549    ///   3. Quantisation per Pseudocode 18 inverse:
550    ///      `q = round(sign(c) * (|c|/sf_gain)^(3/4))`.
551    ///   4. ASF entropy coding via HCB5 (signed dim=2, q-range -4..=+4).
552    ///   5. Wrap in v2 IMS TOC + single-substream-group `audio_size` body.
553    ///
554    /// Frame length is derived from the encoder's
555    /// `(fs_index, frame_rate_index)` pair via [`crate::toc::frame_rate_entry`].
556    /// For the default mono 48 kHz / 24 fps configuration `frame.len()` is
557    /// 1920 samples and `max_sfb` is 10 (matching the canned-tone helper).
558    ///
559    /// Per ETSI TS 103 190-1 §5.5 (MDCT) + §5.7 / §5.8 (SIMPLE/ASF) +
560    /// TS 103 190-2 §6.2.1.1 (IMS TOC).
561    pub fn encode_frame_pcm(&mut self, frame: &[f32]) -> Vec<u8> {
562        // Default max_sfb = 40 (≤ 7.5 kHz at tl=1920) preserves
563        // round-48 behaviour for callers that haven't opted in to the
564        // wider-bandwidth encoder.
565        self.encode_frame_pcm_with_max_sfb(frame, 40)
566    }
567
568    /// Encode one IMS v2 mono frame from arbitrary float PCM input
569    /// (range `[-1.0, 1.0]`) at a caller-specified `max_sfb`. Larger
570    /// values widen the encoder's frequency coverage at the cost of
571    /// more bits per frame:
572    ///   * `max_sfb = 40` → bins 0..508 → ~6.35 kHz @ tl=1920
573    ///   * `max_sfb = 50` → bins 0..1216 → ~15.2 kHz @ tl=1920
574    ///   * `max_sfb = 55` → bins 0..1600 → ~20.0 kHz @ tl=1920
575    ///
576    /// `max_sfb` must satisfy `max_sfb <= num_sfb_48(frame_len)` (61 at
577    /// tl=1920). The pad budget scales with max_sfb so the announced
578    /// `audio_size` reliably exceeds the actual emission length.
579    pub fn encode_frame_pcm_with_max_sfb(&mut self, frame: &[f32], max_sfb: u32) -> Vec<u8> {
580        let (_fps_milli, frame_len) =
581            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
582        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
583        assert_eq!(
584            frame.len(),
585            frame_len as usize,
586            "encode_frame_pcm: input length must match frame_len = {frame_len}"
587        );
588        // Force mono — the forward analysis path is mono-only. (Multi-
589        // channel needs SAP/M-S decision + per-channel state which is
590        // queued for round-50+.)
591        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
592        self.channel_mode_value = 0b0;
593        self.channel_mode_bits = 1;
594
595        // 1. Forward MDCT analysis. Lazily build per-encoder state.
596        if self.mdct_state.is_none() || self.mdct_state.as_ref().unwrap().n != frame_len {
597            self.mdct_state = Some(EncoderMdctState::new(frame_len));
598        }
599        let coeffs = self.mdct_state.as_mut().unwrap().analyse_frame(frame);
600
601        // 2-4. Build the substream body (per-band codebook optimiser +
602        // entropy-coding). Pad target scales with max_sfb to keep the
603        // announced audio_size comfortably above the actual emission
604        // length: worst case is ~25 bits/pair (HCB11 with one escape
605        // per pair) × end_bin/2 pairs ≈ 3 × end_bin bytes.
606        let pad_target_bytes = match max_sfb {
607            0..=40 => 2048,
608            41..=50 => 4096,
609            _ => 8192,
610        };
611        let body = build_mono_simple_asf_body_from_pcm_spectrum(
612            frame_len,
613            max_sfb,
614            &coeffs,
615            pad_target_bytes,
616        );
617
618        // 5. Wrap in v2 IMS TOC.
619        let mut bw = BitWriter::new();
620        self.write_toc(&mut bw);
621        bw.align_to_byte();
622        let mut out = bw.finish();
623        out.extend(body);
624        // sequence_counter wraps at 1024.
625        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
626        // Restore caller's channel_mode setting.
627        self.channel_mode_value = saved_mode.0;
628        self.channel_mode_bits = saved_mode.1;
629        out
630    }
631
632    /// Encode one IMS v2 stereo frame from arbitrary float PCM input
633    /// (range `[-1.0, 1.0]`) for both L and R. Returns the produced
634    /// frame bytes.
635    ///
636    /// **Path A — 2× SCE (split-MDCT)** per ETSI TS 103 190-1 §5.3 +
637    /// §4.2.6.3 Table 22 (`stereo_data()` with
638    /// `b_enable_mdct_stereo_proc == 0`): each channel is encoded
639    /// independently with the shared forward analysis pipeline (KBD-
640    /// windowed MDCT, per-band scalefactor, DP-optimal sectioning,
641    /// HCB1..11 codebook selection, SNF emission). No joint M/S coding.
642    ///
643    /// `frame_l` / `frame_r` must each be exactly `frame_len` samples
644    /// long (1920 samples for the default 48 kHz / 24 fps configuration).
645    /// The encoder forces stereo channel mode (`channel_mode_value =
646    /// 0b10`) for this call. The decoder's
647    /// [`crate::asf::parse_stereo_data_body_stateful`] split-MDCT path
648    /// consumes the frame and reconstructs both channels through the
649    /// shared ASF Huffman pipeline.
650    ///
651    /// `max_sfb` defaults to 40 (matching the round-48 mono default,
652    /// covers bins 0..508 ≈ 0..6.35 kHz at tl = 1920) when called via
653    /// [`Self::encode_frame_pcm_stereo`]; use
654    /// [`Self::encode_frame_pcm_stereo_with_max_sfb`] for wider coverage.
655    /// The decoder's split-MDCT branch reads BOTH L and R `max_sfb` with
656    /// the full `n_msfb_bits` width (the spec's `b_side_limited` only
657    /// applies to joint-MDCT stereo per §4.3.6.2), so the encoder isn't
658    /// limited by the narrower `n_side_bits`.
659    ///
660    /// Per ETSI TS 103 190-1 §5.3 + §4.2.6.3 + §5.5 (MDCT) +
661    /// §5.7 / §5.8 (SIMPLE/ASF) + TS 103 190-2 §6.2.1.1 (IMS TOC).
662    pub fn encode_frame_pcm_stereo(&mut self, frame_l: &[f32], frame_r: &[f32]) -> Vec<u8> {
663        // Default max_sfb = 40 (matches the round-48 mono default).
664        self.encode_frame_pcm_stereo_with_max_sfb(frame_l, frame_r, 40)
665    }
666
667    /// Round-52 heuristic threshold for joint M/S coding. When the
668    /// per-SFB average Pearson correlation between L and R MDCT spectra
669    /// exceeds this value, the encoder switches to Path B (joint M/S CPE,
670    /// `b_enable_mdct_stereo_proc == 1`); otherwise Path A (split-MDCT,
671    /// 2× SCE) is used. The 0.7 threshold matches the spec's §5.3
672    /// guidance plus the headline number cited in this crate's round-52
673    /// task brief.
674    pub const STEREO_JOINT_MS_CORRELATION_THRESHOLD: f32 = 0.7;
675
676    /// Encode one IMS v2 stereo frame from arbitrary float PCM input
677    /// (range `[-1.0, 1.0]`) at a caller-specified `max_sfb`. Both
678    /// channels use the same `max_sfb` — the encoder uses the full
679    /// `n_msfb_bits` field width for both. See
680    /// [`Self::encode_frame_pcm_stereo`].
681    ///
682    /// **Round 52 — Path A vs Path B dispatch.** The encoder computes the
683    /// per-SFB average Pearson correlation between the L and R MDCT
684    /// spectra (via [`average_per_sfb_correlation`]) and, when it exceeds
685    /// [`Self::STEREO_JOINT_MS_CORRELATION_THRESHOLD`] (default 0.7),
686    /// switches to **joint M/S CPE (Path B,
687    /// `b_enable_mdct_stereo_proc == 1`)** per ETSI TS 103 190-1 §5.3 +
688    /// §4.2.6.3 Table 22 + §7.5 (Pseudocode 77). Otherwise it stays on
689    /// the round-51 split-MDCT path (Path A, 2× SCE,
690    /// `b_enable_mdct_stereo_proc == 0`). Per-SFB M/S vs L/R selection
691    /// within the joint path is driven by the natural-q bit-cost
692    /// comparison inside
693    /// [`build_stereo_simple_asf_joint_body_from_pcm_spectra`].
694    ///
695    /// Use [`Self::encode_frame_pcm_stereo_split_with_max_sfb`] or
696    /// [`Self::encode_frame_pcm_stereo_joint_with_max_sfb`] to force a
697    /// specific path regardless of correlation.
698    pub fn encode_frame_pcm_stereo_with_max_sfb(
699        &mut self,
700        frame_l: &[f32],
701        frame_r: &[f32],
702        max_sfb: u32,
703    ) -> Vec<u8> {
704        self.encode_frame_pcm_stereo_dispatched(frame_l, frame_r, max_sfb, None)
705    }
706
707    /// Force the split-MDCT (Path A: 2× SCE) encoder path regardless of
708    /// the L-vs-R correlation. Useful for tests / fixtures that need a
709    /// deterministic on-wire layout. See
710    /// [`Self::encode_frame_pcm_stereo_with_max_sfb`].
711    pub fn encode_frame_pcm_stereo_split_with_max_sfb(
712        &mut self,
713        frame_l: &[f32],
714        frame_r: &[f32],
715        max_sfb: u32,
716    ) -> Vec<u8> {
717        self.encode_frame_pcm_stereo_dispatched(frame_l, frame_r, max_sfb, Some(false))
718    }
719
720    /// Force the joint-MDCT (Path B: M/S CPE) encoder path regardless of
721    /// the L-vs-R correlation. Useful for tests / fixtures that need a
722    /// deterministic on-wire layout. See
723    /// [`Self::encode_frame_pcm_stereo_with_max_sfb`].
724    pub fn encode_frame_pcm_stereo_joint_with_max_sfb(
725        &mut self,
726        frame_l: &[f32],
727        frame_r: &[f32],
728        max_sfb: u32,
729    ) -> Vec<u8> {
730        self.encode_frame_pcm_stereo_dispatched(frame_l, frame_r, max_sfb, Some(true))
731    }
732
733    /// Shared body for the stereo encode dispatch — runs forward MDCT,
734    /// computes the cross-channel correlation (when `force_joint` is
735    /// `None`) and picks Path A vs Path B accordingly. `force_joint =
736    /// Some(true)` always emits joint M/S, `Some(false)` always emits
737    /// split-MDCT, `None` selects via the
738    /// [`Self::STEREO_JOINT_MS_CORRELATION_THRESHOLD`] threshold.
739    fn encode_frame_pcm_stereo_dispatched(
740        &mut self,
741        frame_l: &[f32],
742        frame_r: &[f32],
743        max_sfb: u32,
744        force_joint: Option<bool>,
745    ) -> Vec<u8> {
746        let (_fps_milli, frame_len) =
747            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
748        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
749        assert_eq!(
750            frame_l.len(),
751            frame_len as usize,
752            "encode_frame_pcm_stereo: L input length must match frame_len = {frame_len}"
753        );
754        assert_eq!(
755            frame_r.len(),
756            frame_len as usize,
757            "encode_frame_pcm_stereo: R input length must match frame_len = {frame_len}"
758        );
759        // Cap max_sfb at n_msfb_bits=6's max (63 for tl=1920) and at the
760        // transform's actual `num_sfb_48` cap (61 at tl=1920).
761        let (n_msfb_bits, _, _) =
762            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
763        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
764        let max_sfb = max_sfb.min(n_msfb_cap);
765
766        // Force stereo channel_mode (prefix '10', 2 bits) — both the
767        // split-MDCT and joint-MDCT body builders require the TOC to
768        // declare 2 channels.
769        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
770        self.channel_mode_value = 0b10;
771        self.channel_mode_bits = 2;
772
773        // 1. Forward MDCT analysis per channel (separate state for
774        //    independent 50% TDAC overlap continuity).
775        if self.mdct_state.is_none() || self.mdct_state.as_ref().unwrap().n != frame_len {
776            self.mdct_state = Some(EncoderMdctState::new(frame_len));
777        }
778        if self.mdct_state_r.is_none() || self.mdct_state_r.as_ref().unwrap().n != frame_len {
779            self.mdct_state_r = Some(EncoderMdctState::new(frame_len));
780        }
781        let coeffs_l = self.mdct_state.as_mut().unwrap().analyse_frame(frame_l);
782        let coeffs_r = self.mdct_state_r.as_mut().unwrap().analyse_frame(frame_r);
783
784        // 2. Path A vs Path B dispatch per round-52 heuristic.
785        let use_joint = match force_joint {
786            Some(b) => b,
787            None => {
788                let rho = average_per_sfb_correlation(frame_len, max_sfb, &coeffs_l, &coeffs_r);
789                rho >= Self::STEREO_JOINT_MS_CORRELATION_THRESHOLD
790            }
791        };
792
793        // 3-5. Build the stereo body. Pad budget is 2× the mono budget
794        //      since we carry two spectra (joint or split).
795        let pad_target_bytes = match max_sfb {
796            0..=20 => 2048,
797            21..=40 => 4096,
798            41..=50 => 8192,
799            _ => 16384,
800        };
801        let body = if use_joint {
802            build_stereo_simple_asf_joint_body_from_pcm_spectra(
803                frame_len,
804                max_sfb,
805                &coeffs_l,
806                &coeffs_r,
807                pad_target_bytes,
808            )
809        } else {
810            build_stereo_simple_asf_split_body_from_pcm_spectra(
811                frame_len,
812                max_sfb,
813                &coeffs_l,
814                &coeffs_r,
815                pad_target_bytes,
816            )
817        };
818
819        // 6. Wrap in v2 IMS TOC.
820        let mut bw = BitWriter::new();
821        self.write_toc(&mut bw);
822        bw.align_to_byte();
823        let mut out = bw.finish();
824        out.extend(body);
825        // sequence_counter wraps at 1024.
826        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
827        // Restore caller's channel_mode setting.
828        self.channel_mode_value = saved_mode.0;
829        self.channel_mode_bits = saved_mode.1;
830        out
831    }
832
833    /// Encode one IMS v2 5.0 frame from arbitrary float PCM input (range
834    /// `[-1.0, 1.0]`) for L, R, C, Ls, Rs.
835    ///
836    /// **Path SIMPLE/Cfg3Five — 5 SCE multichannel forward analysis** per
837    /// ETSI TS 103 190-1 §4.2.6.6 Table 25 row `case SIMPLE: coding_config ==
838    /// 3` + §4.2.7.5 Table 29 (`five_channel_data()`): each of the five
839    /// channels is encoded independently with the shared forward-analysis
840    /// pipeline (KBD-windowed MDCT, per-band scalefactor, DP-optimal
841    /// section partition, HCB1..11 codebook selection, SNF emission). One
842    /// shared `sf_info(ASF, 0, 0)` precedes the per-channel data; the
843    /// `five_channel_info()` uses identity SAP (`sap_mode = 0` on every
844    /// `chparam_info()`, `chel_matsel = 0`) so no joint-MDCT mixing happens
845    /// at decode time — every output channel comes straight from its own
846    /// `sf_data(ASF)` body. This is the spec-mandated minimum for the 5.0
847    /// SIMPLE path and unblocks the encoder's path to multichannel.
848    ///
849    /// `frames[i]` must each be exactly `frame_len` samples long
850    /// (1920 samples for the default 48 kHz / 24 fps configuration). The
851    /// slice order matches the 5.0 output layout (`L, R, C, Ls, Rs` —
852    /// Table 180 row `coding_config == 3`). The encoder forces the 5.0
853    /// channel mode (`channel_mode_value = 0b1101`, 4 b — Table 85
854    /// channel_mode 3) for this call.
855    ///
856    /// The decoder's [`crate::mch::parse_5x_audio_data_outer`] for
857    /// `channels == 5` (no LFE) consumes the body, IMDCTs each per-channel
858    /// spectrum into slots 0..4, and emits 5-channel interleaved S16 PCM at
859    /// the declared sample rate. There is no companding / ASPX / A-CPL on
860    /// the SIMPLE path so the round-trip is purely the per-channel MDCT
861    /// quantisation noise (≥ 20 dB spectral SNR per channel on tone /
862    /// white-noise fixtures).
863    ///
864    /// `max_sfb` defaults to 40 (matching the round-49 mono default).
865    /// Use [`Self::encode_frame_pcm_5_0_with_max_sfb`] for wider coverage.
866    ///
867    /// Per ETSI TS 103 190-1 §4.2.6.6 + §4.2.7.5 + §5.5 (MDCT) +
868    /// §5.7 / §5.8 (SIMPLE/ASF) + TS 103 190-2 §6.2.1.1 (IMS TOC).
869    pub fn encode_frame_pcm_5_0(&mut self, frames: &[&[f32]; 5]) -> Vec<u8> {
870        // Default max_sfb = 40 (matches the round-48 mono default).
871        self.encode_frame_pcm_5_0_with_max_sfb(frames, 40)
872    }
873
874    /// Encode one IMS v2 5.0 frame from arbitrary float PCM input at a
875    /// caller-specified `max_sfb`. All five channels share the same
876    /// `max_sfb` (the joint `sf_info` header carries one value). See
877    /// [`Self::encode_frame_pcm_5_0`] for the rest of the contract.
878    pub fn encode_frame_pcm_5_0_with_max_sfb(
879        &mut self,
880        frames: &[&[f32]; 5],
881        max_sfb: u32,
882    ) -> Vec<u8> {
883        let (_fps_milli, frame_len) =
884            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
885        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
886        for (ch, f) in frames.iter().enumerate() {
887            assert_eq!(
888                f.len(),
889                frame_len as usize,
890                "encode_frame_pcm_5_0: channel {ch} input length must match frame_len = {frame_len}"
891            );
892        }
893        // Cap max_sfb at n_msfb_bits=6's max (63 for tl=1920) and at the
894        // transform's actual `num_sfb_48` cap (61 at tl=1920).
895        let (n_msfb_bits, _, _) =
896            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
897        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
898        let max_sfb = max_sfb.min(n_msfb_cap);
899
900        // Force 5.0 channel_mode (prefix '1101', 4 bits — Table 85
901        // channel_mode 3). The body builder requires the TOC to declare
902        // 5 channels so the decoder's `walk_ac4_substream` dispatch
903        // routes through `parse_5x_audio_data_outer(b_has_lfe = false)`.
904        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
905        self.channel_mode_value = 0b1101;
906        self.channel_mode_bits = 4;
907
908        // 1. Forward MDCT analysis per channel (separate state for
909        //    independent 50% TDAC overlap continuity).
910        while self.mdct_states_multi.len() < 5 {
911            self.mdct_states_multi
912                .push(EncoderMdctState::new(frame_len));
913        }
914        for state in self.mdct_states_multi.iter_mut() {
915            if state.n != frame_len {
916                *state = EncoderMdctState::new(frame_len);
917            }
918        }
919        // Run analyses sequentially — borrow each state mutably one at a
920        // time so we don't conflict on `self.mdct_states_multi`.
921        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(5);
922        for (ch, f) in frames.iter().enumerate() {
923            let c = self.mdct_states_multi[ch].analyse_frame(f);
924            coeffs_per_channel.push(c);
925        }
926
927        // 2-4. Build the 5.0 SIMPLE/Cfg3Five body. Pad budget is 5× the
928        //      mono budget since we carry five spectra independently.
929        let pad_target_bytes: usize = match max_sfb {
930            0..=20 => 4096,
931            21..=40 => 8192,
932            41..=50 => 16384,
933            _ => 32768,
934        };
935        let body = build_5_0_simple_asf_body_from_pcm_spectra(
936            frame_len,
937            max_sfb,
938            &[
939                &coeffs_per_channel[0],
940                &coeffs_per_channel[1],
941                &coeffs_per_channel[2],
942                &coeffs_per_channel[3],
943                &coeffs_per_channel[4],
944            ],
945            pad_target_bytes,
946        );
947
948        // 5. Wrap in v2 IMS TOC.
949        let mut bw = BitWriter::new();
950        self.write_toc(&mut bw);
951        bw.align_to_byte();
952        let mut out = bw.finish();
953        out.extend(body);
954        // sequence_counter wraps at 1024.
955        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
956        // Restore caller's channel_mode setting.
957        self.channel_mode_value = saved_mode.0;
958        self.channel_mode_bits = saved_mode.1;
959        out
960    }
961
962    /// Encode one IMS v2 5.1 frame from float PCM input per ETSI
963    /// TS 103 190-1 §4.2.6.6 + §4.2.7.5 + TS 103 190-2 §6.2.1.1, building
964    /// on top of the 5.0 Cfg3Five forward analysis path with an extra
965    /// LFE `mono_data(1)` element per Table 25
966    /// (`if (b_has_lfe) mono_data(1);`).
967    ///
968    /// `frames` is in `[L, R, C, Ls, Rs, LFE]` order. Each slice must
969    /// have length `frame_len` (1920 for the default 48 kHz / 24 fps
970    /// configuration); panics otherwise.
971    ///
972    /// The encoder forces the 5.1 channel_mode prefix (`0b1110`, 4 b —
973    /// Table 85 channel_mode 4) so the decoder's
974    /// `walk_ac4_substream` dispatches `channels == 6` through
975    /// `parse_5x_audio_data_outer(b_has_lfe = true)`. The LFE channel
976    /// is coded with `sf_info_lfe()` (Table 35) carrying `max_sfb` in
977    /// `n_msfbl_bits` bits (Table 106 column 4 — 3 bits for `tl = 1920`
978    /// → max_sfb_lfe is capped at 7). The five non-LFE channels share
979    /// the same Cfg3Five `five_channel_data()` body as the 5.0 path
980    /// (identity SAP, independent per-channel SCE).
981    ///
982    /// `max_sfb` defaults to 40 (matching the round-49 mono / round-74
983    /// 5.0 default); `max_sfb_lfe` defaults to 7 (the LFE-spec cap at
984    /// `tl = 1920`). Use [`Self::encode_frame_pcm_5_1_with_max_sfb`]
985    /// for wider coverage.
986    pub fn encode_frame_pcm_5_1(&mut self, frames: &[&[f32]; 6]) -> Vec<u8> {
987        self.encode_frame_pcm_5_1_with_max_sfb(frames, 40, 7)
988    }
989
990    /// Encode one IMS v2 5.1 frame from arbitrary float PCM input at
991    /// caller-specified `max_sfb` (non-LFE channels) and `max_sfb_lfe`
992    /// (LFE channel). See [`Self::encode_frame_pcm_5_1`] for the rest of
993    /// the contract.
994    pub fn encode_frame_pcm_5_1_with_max_sfb(
995        &mut self,
996        frames: &[&[f32]; 6],
997        max_sfb: u32,
998        max_sfb_lfe: u32,
999    ) -> Vec<u8> {
1000        let (_fps_milli, frame_len) =
1001            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
1002        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
1003        for (ch, f) in frames.iter().enumerate() {
1004            assert_eq!(
1005                f.len(),
1006                frame_len as usize,
1007                "encode_frame_pcm_5_1: channel {ch} input length must match frame_len = {frame_len}"
1008            );
1009        }
1010        // Cap max_sfb at the non-LFE max_sfb width's max and at the actual
1011        // num_sfb_48 cap.
1012        let (n_msfb_bits, _, n_msfbl_bits) =
1013            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
1014        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
1015        let max_sfb = max_sfb.min(n_msfb_cap);
1016        assert!(
1017            n_msfbl_bits > 0,
1018            "encode_frame_pcm_5_1: tl = {frame_len} not permitted for LFE"
1019        );
1020        let n_msfbl_cap = (1u32 << n_msfbl_bits) - 1;
1021        let max_sfb_lfe = max_sfb_lfe.min(n_msfbl_cap);
1022
1023        // Force 5.1 channel_mode (prefix '1110', 4 bits — Table 85
1024        // channel_mode 4). The body builder requires the TOC to declare
1025        // 6 channels so the decoder's `walk_ac4_substream` dispatch
1026        // routes through `parse_5x_audio_data_outer(b_has_lfe = true)`.
1027        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
1028        self.channel_mode_value = 0b1110;
1029        self.channel_mode_bits = 4;
1030
1031        // 1. Forward MDCT analysis per channel (separate state for
1032        //    independent 50% TDAC overlap continuity). Six channels here
1033        //    so the multi-channel state vector needs to grow.
1034        while self.mdct_states_multi.len() < 6 {
1035            self.mdct_states_multi
1036                .push(EncoderMdctState::new(frame_len));
1037        }
1038        for state in self.mdct_states_multi.iter_mut() {
1039            if state.n != frame_len {
1040                *state = EncoderMdctState::new(frame_len);
1041            }
1042        }
1043        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(6);
1044        for (ch, f) in frames.iter().enumerate() {
1045            let c = self.mdct_states_multi[ch].analyse_frame(f);
1046            coeffs_per_channel.push(c);
1047        }
1048
1049        // 2-4. Build the 5.1 SIMPLE/Cfg3Five body. Pad budget is 6× the
1050        //      mono budget since we carry six spectra independently.
1051        let pad_target_bytes: usize = match max_sfb {
1052            0..=20 => 4096,
1053            21..=40 => 8192,
1054            41..=50 => 16384,
1055            _ => 32768,
1056        };
1057        let body = build_5_1_simple_asf_body_from_pcm_spectra(
1058            frame_len,
1059            max_sfb,
1060            max_sfb_lfe,
1061            &[
1062                &coeffs_per_channel[0],
1063                &coeffs_per_channel[1],
1064                &coeffs_per_channel[2],
1065                &coeffs_per_channel[3],
1066                &coeffs_per_channel[4],
1067                &coeffs_per_channel[5],
1068            ],
1069            pad_target_bytes,
1070        );
1071
1072        // 5. Wrap in v2 IMS TOC.
1073        let mut bw = BitWriter::new();
1074        self.write_toc(&mut bw);
1075        bw.align_to_byte();
1076        let mut out = bw.finish();
1077        out.extend(body);
1078        // sequence_counter wraps at 1024.
1079        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
1080        // Restore caller's channel_mode setting.
1081        self.channel_mode_value = saved_mode.0;
1082        self.channel_mode_bits = saved_mode.1;
1083        out
1084    }
1085
1086    /// Encode one IMS v2 7.0 (3/4/0) frame from float PCM input per ETSI
1087    /// TS 103 190-1 §4.2.6.14 Table 33 + §4.2.7.5 Table 29
1088    /// (`five_channel_data()`) + §4.2.7.4 Table 26 (`two_channel_data()`).
1089    /// The non-LFE immersive counterpart of
1090    /// [`Self::encode_frame_pcm_7_1`] — same `7_X_codec_mode = SIMPLE` +
1091    /// `coding_config = Cfg3Five` body shape, but the walker's
1092    /// `if (b_has_lfe) mono_data(1);` branch is omitted (`b_has_lfe =
1093    /// false` for channel_mode 5 / 7.0).
1094    ///
1095    /// `frames` is in `[L, R, C, Ls, Rs, Lb, Rb]` order — the inner
1096    /// `five_channel_data()` (Table 180) carries the front/surround pair
1097    /// `L/R/C/Ls/Rs` and the SIMPLE/ASPX additional-channel block carries
1098    /// the immersive back pair `Lb/Rb` via a trailing
1099    /// `two_channel_data()` per Table 26. The encoder uses identity SAP
1100    /// (`b_use_sap_add_ch = 0`, `sap_mode = 0` on every `chparam_info`)
1101    /// so no joint-MDCT mixing happens at decode time: every output
1102    /// channel comes straight from its own `sf_data(ASF)` body.
1103    ///
1104    /// The encoder forces the 7.0 channel_mode prefix (`0b1111000`, 7 b —
1105    /// Table 88 channel_mode 5) so the decoder's `walk_ac4_substream`
1106    /// dispatches `channels == 7` through
1107    /// `parse_7x_audio_data_outer(b_has_lfe = false)`. The five
1108    /// front/surround channels share the same Cfg3Five
1109    /// `five_channel_data()` body as the 5.0 / 5.1 / 7.1 paths; the
1110    /// additional pair (Lb, Rb) rides the trailing `two_channel_data()`
1111    /// which the decoder's `dispatch_7x_additional_channel_pair` (Table
1112    /// 183 row "3/4/0.x" identity path) routes into output slots 5 / 6.
1113    ///
1114    /// `max_sfb` defaults to 40 (matching the round-49 mono / round-74
1115    /// 5.0 / round-80 5.1 / round-91 7.1 default); `max_sfb_add`
1116    /// defaults to 40 (same width as the 7.0 non-additional channels).
1117    /// Use [`Self::encode_frame_pcm_7_0_with_max_sfb`] for wider coverage.
1118    pub fn encode_frame_pcm_7_0(&mut self, frames: &[&[f32]; 7]) -> Vec<u8> {
1119        self.encode_frame_pcm_7_0_with_max_sfb(frames, 40, 40)
1120    }
1121
1122    /// Encode one IMS v2 7.0 (3/4/0) frame from arbitrary float PCM input
1123    /// at caller-specified `max_sfb` (five-channel front/surround SCEs)
1124    /// and `max_sfb_add` (additional Lb/Rb pair). See
1125    /// [`Self::encode_frame_pcm_7_0`] for the rest of the contract.
1126    pub fn encode_frame_pcm_7_0_with_max_sfb(
1127        &mut self,
1128        frames: &[&[f32]; 7],
1129        max_sfb: u32,
1130        max_sfb_add: u32,
1131    ) -> Vec<u8> {
1132        let (_fps_milli, frame_len) =
1133            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
1134        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
1135        for (ch, f) in frames.iter().enumerate() {
1136            assert_eq!(
1137                f.len(),
1138                frame_len as usize,
1139                "encode_frame_pcm_7_0: channel {ch} input length must match frame_len = {frame_len}"
1140            );
1141        }
1142        // Cap max_sfb at the non-LFE max_sfb width's max and at the actual
1143        // num_sfb_48 cap. Same cap applies to both the inner
1144        // five_channel_data and the additional two_channel_data — they
1145        // share the n_msfb_bits width.
1146        let (n_msfb_bits, _, _) =
1147            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
1148        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
1149        let max_sfb = max_sfb.min(n_msfb_cap);
1150        let max_sfb_add = max_sfb_add.min(n_msfb_cap);
1151
1152        // Force 7.0 (3/4/0) channel_mode (prefix '1111000', 7 bits —
1153        // Table 88 channel_mode 5). The body builder requires the TOC to
1154        // declare 7 channels so the decoder's `walk_ac4_substream`
1155        // dispatch routes through `parse_7x_audio_data_outer(b_has_lfe =
1156        // false)`.
1157        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
1158        self.channel_mode_value = 0b1111000;
1159        self.channel_mode_bits = 7;
1160
1161        // 1. Forward MDCT analysis per channel (separate state for
1162        //    independent 50% TDAC overlap continuity). Seven channels
1163        //    here so the multi-channel state vector needs to grow.
1164        while self.mdct_states_multi.len() < 7 {
1165            self.mdct_states_multi
1166                .push(EncoderMdctState::new(frame_len));
1167        }
1168        for state in self.mdct_states_multi.iter_mut() {
1169            if state.n != frame_len {
1170                *state = EncoderMdctState::new(frame_len);
1171            }
1172        }
1173        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(7);
1174        for (ch, f) in frames.iter().enumerate() {
1175            let c = self.mdct_states_multi[ch].analyse_frame(f);
1176            coeffs_per_channel.push(c);
1177        }
1178
1179        // 2-4. Build the 7.0 SIMPLE/Cfg3Five body. Pad budget is ~7× the
1180        //      mono budget since we carry seven spectra independently.
1181        //      Capped at 32767 — the 15-bit `audio_size_value` field
1182        //      saturates there (extending via `b_more_bits` is permitted
1183        //      by §4.3.4.1 but not needed for the default max_sfb path).
1184        let pad_target_bytes: usize = match max_sfb {
1185            0..=20 => 4096,
1186            21..=40 => 12288,
1187            41..=50 => 24576,
1188            _ => 32767,
1189        };
1190        let body = build_7_0_simple_asf_body_from_pcm_spectra(
1191            frame_len,
1192            max_sfb,
1193            max_sfb_add,
1194            &[
1195                &coeffs_per_channel[0],
1196                &coeffs_per_channel[1],
1197                &coeffs_per_channel[2],
1198                &coeffs_per_channel[3],
1199                &coeffs_per_channel[4],
1200                &coeffs_per_channel[5],
1201                &coeffs_per_channel[6],
1202            ],
1203            pad_target_bytes,
1204        );
1205
1206        // 5. Wrap in v2 IMS TOC.
1207        let mut bw = BitWriter::new();
1208        self.write_toc(&mut bw);
1209        bw.align_to_byte();
1210        let mut out = bw.finish();
1211        out.extend(body);
1212        // sequence_counter wraps at 1024.
1213        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
1214        // Restore caller's channel_mode setting.
1215        self.channel_mode_value = saved_mode.0;
1216        self.channel_mode_bits = saved_mode.1;
1217        out
1218    }
1219
1220    /// Encode one IMS v2 7.1 (3/4/0.1) frame from float PCM input per ETSI
1221    /// TS 103 190-1 §4.2.6.14 Table 33 + §4.2.7.5 Table 29
1222    /// (`five_channel_data()`) + §4.2.7.4 Table 26 (`two_channel_data()`),
1223    /// building on top of the 5.1 Cfg3Five forward analysis path with an
1224    /// extra trailing `two_channel_data()` for the immersive
1225    /// additional-channel pair (Lb, Rb) per the SIMPLE/ASPX
1226    /// additional-channel block in §4.2.6.14:
1227    /// `b_use_sap_add_ch + two_channel_data()`.
1228    ///
1229    /// `frames` is in `[L, R, C, Ls, Rs, Lb, Rb, LFE]` order (matching
1230    /// the decoder's output slot convention: slots 0..4 from
1231    /// `five_channel_data()` per Table 180, slots 5/6 from the additional
1232    /// `two_channel_data()` per `dispatch_7x_additional_channel_pair` /
1233    /// Table 183 row "3/4/0.x" identity-SAP path, slot 7 from the LFE
1234    /// `mono_data(1)`). Each slice must have length `frame_len` (1920
1235    /// for the default 48 kHz / 24 fps configuration); panics otherwise.
1236    ///
1237    /// The encoder forces the 7.1 (3/4/0.1) channel_mode prefix
1238    /// (`0b1111001`, 7 b — Table 88 channel_mode 6) so the decoder's
1239    /// `walk_ac4_substream` dispatches `channels == 8` through
1240    /// `parse_7x_audio_data_outer(b_has_lfe = true)`. The LFE channel
1241    /// is coded with `sf_info_lfe()` (Table 35) carrying `max_sfb` in
1242    /// `n_msfbl_bits` bits (Table 106 column 4 — 3 bits for `tl = 1920`
1243    /// → max_sfb_lfe is capped at 7). The five non-LFE front/surround
1244    /// channels share the same Cfg3Five `five_channel_data()` body as
1245    /// the 5.1 path; the additional pair (Lb, Rb) is coded as a single
1246    /// `two_channel_data()` with identity SAP (`b_use_sap_add_ch = 0`,
1247    /// `sap_mode = 0` on its `chparam_info`) so no joint-MDCT mixing
1248    /// happens at decode time and slots 5/6 receive Lb/Rb directly.
1249    ///
1250    /// `max_sfb` defaults to 40 (matching the round-49 mono / round-74
1251    /// 5.0 / round-80 5.1 default); `max_sfb_add` defaults to 40 (same
1252    /// width as the 5.1 non-LFE channels); `max_sfb_lfe` defaults to 7
1253    /// (the LFE-spec cap at `tl = 1920`). Use
1254    /// [`Self::encode_frame_pcm_7_1_with_max_sfb`] for wider coverage.
1255    pub fn encode_frame_pcm_7_1(&mut self, frames: &[&[f32]; 8]) -> Vec<u8> {
1256        self.encode_frame_pcm_7_1_with_max_sfb(frames, 40, 40, 7)
1257    }
1258
1259    /// Encode one IMS v2 7.1 (3/4/0.1) frame from arbitrary float PCM
1260    /// input at caller-specified `max_sfb` (five-channel front/surround
1261    /// SCEs), `max_sfb_add` (additional Lb/Rb pair), and `max_sfb_lfe`
1262    /// (LFE channel). See [`Self::encode_frame_pcm_7_1`] for the rest of
1263    /// the contract.
1264    pub fn encode_frame_pcm_7_1_with_max_sfb(
1265        &mut self,
1266        frames: &[&[f32]; 8],
1267        max_sfb: u32,
1268        max_sfb_add: u32,
1269        max_sfb_lfe: u32,
1270    ) -> Vec<u8> {
1271        let (_fps_milli, frame_len) =
1272            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
1273        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
1274        for (ch, f) in frames.iter().enumerate() {
1275            assert_eq!(
1276                f.len(),
1277                frame_len as usize,
1278                "encode_frame_pcm_7_1: channel {ch} input length must match frame_len = {frame_len}"
1279            );
1280        }
1281        // Cap max_sfb at the non-LFE max_sfb width's max and at the actual
1282        // num_sfb_48 cap. Same cap applies to both the inner
1283        // five_channel_data and the additional two_channel_data — they
1284        // share the n_msfb_bits width.
1285        let (n_msfb_bits, _, n_msfbl_bits) =
1286            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
1287        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
1288        let max_sfb = max_sfb.min(n_msfb_cap);
1289        let max_sfb_add = max_sfb_add.min(n_msfb_cap);
1290        assert!(
1291            n_msfbl_bits > 0,
1292            "encode_frame_pcm_7_1: tl = {frame_len} not permitted for LFE"
1293        );
1294        let n_msfbl_cap = (1u32 << n_msfbl_bits) - 1;
1295        let max_sfb_lfe = max_sfb_lfe.min(n_msfbl_cap);
1296
1297        // Force 7.1 (3/4/0.1) channel_mode (prefix '1111001', 7 bits —
1298        // Table 88 channel_mode 6). The body builder requires the TOC to
1299        // declare 8 channels so the decoder's `walk_ac4_substream`
1300        // dispatch routes through `parse_7x_audio_data_outer(b_has_lfe =
1301        // true)`.
1302        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
1303        self.channel_mode_value = 0b1111001;
1304        self.channel_mode_bits = 7;
1305
1306        // 1. Forward MDCT analysis per channel (separate state for
1307        //    independent 50% TDAC overlap continuity). Eight channels
1308        //    here so the multi-channel state vector needs to grow.
1309        while self.mdct_states_multi.len() < 8 {
1310            self.mdct_states_multi
1311                .push(EncoderMdctState::new(frame_len));
1312        }
1313        for state in self.mdct_states_multi.iter_mut() {
1314            if state.n != frame_len {
1315                *state = EncoderMdctState::new(frame_len);
1316            }
1317        }
1318        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(8);
1319        for (ch, f) in frames.iter().enumerate() {
1320            let c = self.mdct_states_multi[ch].analyse_frame(f);
1321            coeffs_per_channel.push(c);
1322        }
1323
1324        // 2-4. Build the 7.1 SIMPLE/Cfg3Five body. Pad budget is ~8× the
1325        //      mono budget since we carry eight spectra independently.
1326        //      Capped at 32767 — the 15-bit `audio_size_value` field
1327        //      saturates there (extending via `b_more_bits` is permitted
1328        //      by §4.3.4.1 but not needed for the default max_sfb path).
1329        let pad_target_bytes: usize = match max_sfb {
1330            0..=20 => 4096,
1331            21..=40 => 12288,
1332            41..=50 => 24576,
1333            _ => 32767,
1334        };
1335        let body = build_7_1_simple_asf_body_from_pcm_spectra(
1336            frame_len,
1337            max_sfb,
1338            max_sfb_add,
1339            max_sfb_lfe,
1340            &[
1341                &coeffs_per_channel[0],
1342                &coeffs_per_channel[1],
1343                &coeffs_per_channel[2],
1344                &coeffs_per_channel[3],
1345                &coeffs_per_channel[4],
1346                &coeffs_per_channel[5],
1347                &coeffs_per_channel[6],
1348                &coeffs_per_channel[7],
1349            ],
1350            pad_target_bytes,
1351        );
1352
1353        // 5. Wrap in v2 IMS TOC.
1354        let mut bw = BitWriter::new();
1355        self.write_toc(&mut bw);
1356        bw.align_to_byte();
1357        let mut out = bw.finish();
1358        out.extend(body);
1359        // sequence_counter wraps at 1024.
1360        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
1361        // Restore caller's channel_mode setting.
1362        self.channel_mode_value = saved_mode.0;
1363        self.channel_mode_bits = saved_mode.1;
1364        out
1365    }
1366
1367    /// Encode one IMS v2 5.X frame in 5_X_codec_mode = ASPX_ACPL_3 per
1368    /// ETSI TS 103 190-1 §4.2.6.6 Table 25 row `case ASPX_ACPL_3:`
1369    /// (round 95). Symmetric counterpart to the decoder's round-34
1370    /// [`crate::mch::parse_5x_audio_data_outer`] ASPX_ACPL_3 walker.
1371    ///
1372    /// `frames` is in `[L, R, C]` order — three carrier channels. The
1373    /// L/R pair feeds the `stereo_data()` body (split-MDCT path) and
1374    /// drives the A-CPL Ls/Rs surround reconstruction via Pseudocode 118
1375    /// at decode time. The centre carrier `C` is present in the
1376    /// `coeffs_per_channel` slice but unused on the spec ASPX_ACPL_3
1377    /// path — the decoder reconstructs the centre from
1378    /// `cfg0_centre_mono.scaled_spec` (which the ASPX_ACPL_3 walker
1379    /// doesn't populate), so the decoder's centre output is zero-filled.
1380    ///
1381    /// The encoder forces the 5.0 channel_mode prefix (`0b1101`, 4 b —
1382    /// Table 85 channel_mode 3) so the decoder's `walk_ac4_substream`
1383    /// dispatches `channels == 5` through
1384    /// `parse_5x_audio_data_outer(b_has_lfe = false)` with
1385    /// `5_X_codec_mode = AspxAcpl3`.
1386    ///
1387    /// The ASPX/A-CPL parameter bits are emitted as
1388    /// minimum-bit-cost zero-delta Huffman codewords (per round-95
1389    /// "structural scaffold" mode — see [`crate::encoder_acpl3`]).
1390    /// The decoder walks the full Table 25 body and produces
1391    /// 5-channel `[L, R, C, Ls, Rs]` PCM via
1392    /// [`crate::acpl_synth::run_acpl_5x_mch_pcm`]. With all-zero ACPL
1393    /// parameter deltas the surround pair Ls/Rs collapses to the
1394    /// ducker-driven reconstruction from the L/R carriers.
1395    ///
1396    /// `max_sfb` defaults to 40 (matching the round-49 mono / round-74
1397    /// 5.0 default).
1398    pub fn encode_frame_pcm_5_0_acpl3(&mut self, frames: &[&[f32]; 3]) -> Vec<u8> {
1399        self.encode_frame_pcm_5_x_acpl3_with_max_sfb(frames, None, 40, None)
1400    }
1401
1402    /// Encode one IMS v2 5.1 frame in 5_X_codec_mode = ASPX_ACPL_3 with
1403    /// an LFE channel per ETSI TS 103 190-1 §4.2.6.6 Table 25
1404    /// (`if (b_has_lfe) mono_data(1);`) + §4.2.8 (`sf_info_lfe()`).
1405    ///
1406    /// `frames` is in `[L, R, C, LFE]` order. The L/R carrier pair drives
1407    /// the stereo body + A-CPL Ls/Rs reconstruction (same as
1408    /// [`Self::encode_frame_pcm_5_0_acpl3`]); the LFE channel is coded
1409    /// as a leading `mono_data(b_lfe = 1)` element per Table 21.
1410    pub fn encode_frame_pcm_5_1_acpl3(&mut self, frames: &[&[f32]; 4]) -> Vec<u8> {
1411        // Decompose into the 3-carrier slice + the LFE slice for the
1412        // shared dispatcher.
1413        let carriers: [&[f32]; 3] = [frames[0], frames[1], frames[2]];
1414        self.encode_frame_pcm_5_x_acpl3_with_max_sfb(&carriers, Some(frames[3]), 40, Some(7))
1415    }
1416
1417    /// Shared body for [`Self::encode_frame_pcm_5_0_acpl3`] and
1418    /// [`Self::encode_frame_pcm_5_1_acpl3`]. `frames` carries the three
1419    /// non-LFE channels (`[L, R, C]`); `lfe` is `Some` for the 5.1 path,
1420    /// `None` for 5.0.
1421    fn encode_frame_pcm_5_x_acpl3_with_max_sfb(
1422        &mut self,
1423        frames: &[&[f32]; 3],
1424        lfe: Option<&[f32]>,
1425        max_sfb: u32,
1426        max_sfb_lfe: Option<u32>,
1427    ) -> Vec<u8> {
1428        let (_fps_milli, frame_len) =
1429            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
1430        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
1431        for (ch, f) in frames.iter().enumerate() {
1432            assert_eq!(
1433                f.len(),
1434                frame_len as usize,
1435                "encode_frame_pcm_5_x_acpl3: channel {ch} input length must match frame_len = {frame_len}"
1436            );
1437        }
1438        if let Some(lfe_buf) = lfe {
1439            assert_eq!(
1440                lfe_buf.len(),
1441                frame_len as usize,
1442                "encode_frame_pcm_5_x_acpl3: LFE input length must match frame_len = {frame_len}"
1443            );
1444        }
1445        let (n_msfb_bits, _, _) =
1446            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
1447        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
1448        let max_sfb = max_sfb.min(n_msfb_cap);
1449
1450        // Force the right channel_mode based on whether LFE is present.
1451        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
1452        if lfe.is_some() {
1453            // 5.1 channel_mode prefix '1110', 4 b → channel_mode 4.
1454            self.channel_mode_value = 0b1110;
1455            self.channel_mode_bits = 4;
1456        } else {
1457            // 5.0 channel_mode prefix '1101', 4 b → channel_mode 3.
1458            self.channel_mode_value = 0b1101;
1459            self.channel_mode_bits = 4;
1460        }
1461
1462        // 1. Forward MDCT analysis per channel (separate state for
1463        //    independent 50% TDAC overlap continuity).
1464        let n_channels = if lfe.is_some() { 4 } else { 3 };
1465        while self.mdct_states_multi.len() < n_channels {
1466            self.mdct_states_multi
1467                .push(EncoderMdctState::new(frame_len));
1468        }
1469        for state in self.mdct_states_multi.iter_mut() {
1470            if state.n != frame_len {
1471                *state = EncoderMdctState::new(frame_len);
1472            }
1473        }
1474        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
1475        for (ch, f) in frames.iter().enumerate() {
1476            let c = self.mdct_states_multi[ch].analyse_frame(f);
1477            coeffs_per_channel.push(c);
1478        }
1479        let coeffs_lfe: Option<Vec<f32>> = lfe.map(|buf| {
1480            // LFE channel uses its own MDCT state at index 3.
1481            self.mdct_states_multi[3].analyse_frame(buf)
1482        });
1483
1484        // 2. Build the ASPX_ACPL_3 body via the shared builder. ASPX
1485        //    config: small low-res scale so the SBG counts stay small —
1486        //    keeps the ASPX_data_2ch body compact.
1487        let aspx_cfg = crate::aspx::AspxConfig {
1488            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
1489            start_freq: 0,
1490            stop_freq: 0,
1491            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
1492            interpolation: false,
1493            preflat: false,
1494            limiter: false,
1495            noise_sbg: 0, // num_noise_sbgroups = 1
1496            num_env_bits_fixfix: 0,
1497            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
1498        };
1499
1500        // ACPL: num_param_bands_id = 3 → 7 param bands; quant_modes Fine.
1501        let acpl_num_param_bands_id: u8 = 3;
1502        let acpl_qm0 = crate::acpl::AcplQuantMode::Fine;
1503        let acpl_qm1 = crate::acpl::AcplQuantMode::Fine;
1504
1505        // Pad budget: scale with max_sfb and channel count (3 or 4).
1506        let pad_target_bytes: usize = match max_sfb {
1507            0..=20 => 4096,
1508            21..=40 => 8192,
1509            41..=50 => 16384,
1510            _ => 32768,
1511        };
1512
1513        let body = crate::encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra(
1514            frame_len,
1515            max_sfb,
1516            max_sfb_lfe,
1517            self.b_iframe_global,
1518            &coeffs_per_channel[0],
1519            &coeffs_per_channel[1],
1520            coeffs_lfe.as_deref(),
1521            &aspx_cfg,
1522            acpl_num_param_bands_id,
1523            acpl_qm0,
1524            acpl_qm1,
1525            pad_target_bytes,
1526        );
1527
1528        // 3. Wrap in v2 IMS TOC.
1529        let mut bw = BitWriter::new();
1530        self.write_toc(&mut bw);
1531        bw.align_to_byte();
1532        let mut out = bw.finish();
1533        out.extend(body);
1534        // sequence_counter wraps at 1024.
1535        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
1536        // Restore caller's channel_mode setting.
1537        self.channel_mode_value = saved_mode.0;
1538        self.channel_mode_bits = saved_mode.1;
1539        out
1540    }
1541
1542    /// Encode one IMS v2 5.0 frame in 5_X_codec_mode = ASPX_ACPL_3 with
1543    /// real per-parameter-band β1 / β2 extraction from the L / R
1544    /// carrier energy distributions (round 193). Symmetric to
1545    /// [`Self::encode_frame_pcm_5_0_acpl3`] but routes the substream
1546    /// body builder through
1547    /// [`crate::encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_beta`]
1548    /// so the β1 / β2 Huffman layers carry the carrier-driven decorrelator
1549    /// gains instead of all-zero codewords.
1550    ///
1551    /// `beta_scale` controls the wet/dry balance — see
1552    /// [`crate::encoder_acpl3::extract_beta_q_per_band_carrier_energy`]
1553    /// for the magnitude / scale relationship. Values in `0.05..=0.3`
1554    /// produce noticeable surround reconstruction without saturating
1555    /// the BETA codebook.
1556    pub fn encode_frame_pcm_5_0_acpl3_real_beta(
1557        &mut self,
1558        frames: &[&[f32]; 3],
1559        beta_scale: f32,
1560    ) -> Vec<u8> {
1561        self.encode_frame_pcm_5_x_acpl3_real_beta_with_max_sfb(frames, None, 40, None, beta_scale)
1562    }
1563
1564    /// 5.1 counterpart to [`Self::encode_frame_pcm_5_0_acpl3_real_beta`].
1565    /// `frames` is in `[L, R, C, LFE]` order. The LFE channel is coded
1566    /// as a leading `mono_data(b_lfe = 1)` element per Table 21 — same
1567    /// path as [`Self::encode_frame_pcm_5_1_acpl3`].
1568    pub fn encode_frame_pcm_5_1_acpl3_real_beta(
1569        &mut self,
1570        frames: &[&[f32]; 4],
1571        beta_scale: f32,
1572    ) -> Vec<u8> {
1573        let carriers: [&[f32]; 3] = [frames[0], frames[1], frames[2]];
1574        self.encode_frame_pcm_5_x_acpl3_real_beta_with_max_sfb(
1575            &carriers,
1576            Some(frames[3]),
1577            40,
1578            Some(7),
1579            beta_scale,
1580        )
1581    }
1582
1583    /// Shared body for the real-β ACPL_3 entry points. Mirrors
1584    /// [`Self::encode_frame_pcm_5_x_acpl3_with_max_sfb`] but invokes the
1585    /// real-β builder. Kept close to the parent so the two paths stay
1586    /// in sync as the ASPX / ACPL config evolves.
1587    fn encode_frame_pcm_5_x_acpl3_real_beta_with_max_sfb(
1588        &mut self,
1589        frames: &[&[f32]; 3],
1590        lfe: Option<&[f32]>,
1591        max_sfb: u32,
1592        max_sfb_lfe: Option<u32>,
1593        beta_scale: f32,
1594    ) -> Vec<u8> {
1595        let (_fps_milli, frame_len) =
1596            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
1597        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
1598        for (ch, f) in frames.iter().enumerate() {
1599            assert_eq!(
1600                f.len(),
1601                frame_len as usize,
1602                "encode_frame_pcm_5_x_acpl3_real_beta: channel {ch} input length must match frame_len = {frame_len}"
1603            );
1604        }
1605        if let Some(lfe_buf) = lfe {
1606            assert_eq!(
1607                lfe_buf.len(),
1608                frame_len as usize,
1609                "encode_frame_pcm_5_x_acpl3_real_beta: LFE input length must match frame_len = {frame_len}"
1610            );
1611        }
1612        let (n_msfb_bits, _, _) =
1613            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
1614        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
1615        let max_sfb = max_sfb.min(n_msfb_cap);
1616
1617        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
1618        if lfe.is_some() {
1619            self.channel_mode_value = 0b1110;
1620            self.channel_mode_bits = 4;
1621        } else {
1622            self.channel_mode_value = 0b1101;
1623            self.channel_mode_bits = 4;
1624        }
1625
1626        let n_channels = if lfe.is_some() { 4 } else { 3 };
1627        while self.mdct_states_multi.len() < n_channels {
1628            self.mdct_states_multi
1629                .push(EncoderMdctState::new(frame_len));
1630        }
1631        for state in self.mdct_states_multi.iter_mut() {
1632            if state.n != frame_len {
1633                *state = EncoderMdctState::new(frame_len);
1634            }
1635        }
1636        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
1637        for (ch, f) in frames.iter().enumerate() {
1638            let c = self.mdct_states_multi[ch].analyse_frame(f);
1639            coeffs_per_channel.push(c);
1640        }
1641        let coeffs_lfe: Option<Vec<f32>> =
1642            lfe.map(|buf| self.mdct_states_multi[3].analyse_frame(buf));
1643
1644        let aspx_cfg = crate::aspx::AspxConfig {
1645            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
1646            start_freq: 0,
1647            stop_freq: 0,
1648            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
1649            interpolation: false,
1650            preflat: false,
1651            limiter: false,
1652            noise_sbg: 0,
1653            num_env_bits_fixfix: 0,
1654            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
1655        };
1656
1657        let acpl_num_param_bands_id: u8 = 3;
1658        let acpl_qm0 = crate::acpl::AcplQuantMode::Fine;
1659        let acpl_qm1 = crate::acpl::AcplQuantMode::Fine;
1660
1661        let pad_target_bytes: usize = match max_sfb {
1662            0..=20 => 4096,
1663            21..=40 => 8192,
1664            41..=50 => 16384,
1665            _ => 32768,
1666        };
1667
1668        let body = crate::encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_beta(
1669            frame_len,
1670            max_sfb,
1671            max_sfb_lfe,
1672            self.b_iframe_global,
1673            &coeffs_per_channel[0],
1674            &coeffs_per_channel[1],
1675            coeffs_lfe.as_deref(),
1676            &aspx_cfg,
1677            acpl_num_param_bands_id,
1678            acpl_qm0,
1679            acpl_qm1,
1680            beta_scale,
1681            pad_target_bytes,
1682        );
1683
1684        let mut bw = BitWriter::new();
1685        self.write_toc(&mut bw);
1686        bw.align_to_byte();
1687        let mut out = bw.finish();
1688        out.extend(body);
1689        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
1690        self.channel_mode_value = saved_mode.0;
1691        self.channel_mode_bits = saved_mode.1;
1692        out
1693    }
1694
1695    /// Encode one IMS v2 5.0 frame in 5_X_codec_mode = ASPX_ACPL_3 with
1696    /// real per-parameter-band α₁ / α₂ extraction from the L↔R carrier
1697    /// cross-correlation (round 196) **and** the round-193 real β₁ / β₂
1698    /// extraction from the L / R carrier energies. Symmetric counterpart
1699    /// to [`Self::encode_frame_pcm_5_0_acpl3_real_beta`] but routes the
1700    /// substream body builder through
1701    /// [`crate::encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_alpha_beta`]
1702    /// so the α₁ / α₂ Huffman layers carry correlation-driven dry-mix
1703    /// balance indices in addition to the carrier-driven decorrelator
1704    /// gains in β₁ / β₂.
1705    ///
1706    /// `alpha_scale` controls the front/back-balance policy — see
1707    /// [`crate::encoder_acpl3::extract_alpha_q_per_band_carrier_correlation`]
1708    /// for the magnitude / scale relationship. Values in `0.25..=1.0`
1709    /// produce a noticeable front/back bias on correlated content
1710    /// without saturating the ALPHA codebook.
1711    ///
1712    /// `beta_scale` retains its r193 meaning. With
1713    /// `alpha_scale = beta_scale = 0.0` the output is byte-identical to
1714    /// [`Self::encode_frame_pcm_5_0_acpl3`]'s round-95 scaffold.
1715    pub fn encode_frame_pcm_5_0_acpl3_real_alpha_beta(
1716        &mut self,
1717        frames: &[&[f32]; 3],
1718        alpha_scale: f32,
1719        beta_scale: f32,
1720    ) -> Vec<u8> {
1721        self.encode_frame_pcm_5_x_acpl3_real_alpha_beta_with_max_sfb(
1722            frames,
1723            None,
1724            40,
1725            None,
1726            alpha_scale,
1727            beta_scale,
1728        )
1729    }
1730
1731    /// 5.1 counterpart to
1732    /// [`Self::encode_frame_pcm_5_0_acpl3_real_alpha_beta`]. `frames` is
1733    /// in `[L, R, C, LFE]` order. The LFE channel is coded as a leading
1734    /// `mono_data(b_lfe = 1)` element per Table 21 — same path as
1735    /// [`Self::encode_frame_pcm_5_1_acpl3`].
1736    pub fn encode_frame_pcm_5_1_acpl3_real_alpha_beta(
1737        &mut self,
1738        frames: &[&[f32]; 4],
1739        alpha_scale: f32,
1740        beta_scale: f32,
1741    ) -> Vec<u8> {
1742        let carriers: [&[f32]; 3] = [frames[0], frames[1], frames[2]];
1743        self.encode_frame_pcm_5_x_acpl3_real_alpha_beta_with_max_sfb(
1744            &carriers,
1745            Some(frames[3]),
1746            40,
1747            Some(7),
1748            alpha_scale,
1749            beta_scale,
1750        )
1751    }
1752
1753    /// Shared body for the real-α/β ACPL_3 entry points. Mirrors
1754    /// [`Self::encode_frame_pcm_5_x_acpl3_real_beta_with_max_sfb`] but
1755    /// invokes the real-α + real-β builder.
1756    #[allow(clippy::too_many_arguments)]
1757    fn encode_frame_pcm_5_x_acpl3_real_alpha_beta_with_max_sfb(
1758        &mut self,
1759        frames: &[&[f32]; 3],
1760        lfe: Option<&[f32]>,
1761        max_sfb: u32,
1762        max_sfb_lfe: Option<u32>,
1763        alpha_scale: f32,
1764        beta_scale: f32,
1765    ) -> Vec<u8> {
1766        let (_fps_milli, frame_len) =
1767            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
1768        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
1769        for (ch, f) in frames.iter().enumerate() {
1770            assert_eq!(
1771                f.len(),
1772                frame_len as usize,
1773                "encode_frame_pcm_5_x_acpl3_real_alpha_beta: channel {ch} input length must match frame_len = {frame_len}"
1774            );
1775        }
1776        if let Some(lfe_buf) = lfe {
1777            assert_eq!(
1778                lfe_buf.len(),
1779                frame_len as usize,
1780                "encode_frame_pcm_5_x_acpl3_real_alpha_beta: LFE input length must match frame_len = {frame_len}"
1781            );
1782        }
1783        let (n_msfb_bits, _, _) =
1784            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
1785        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
1786        let max_sfb = max_sfb.min(n_msfb_cap);
1787
1788        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
1789        if lfe.is_some() {
1790            self.channel_mode_value = 0b1110;
1791            self.channel_mode_bits = 4;
1792        } else {
1793            self.channel_mode_value = 0b1101;
1794            self.channel_mode_bits = 4;
1795        }
1796
1797        let n_channels = if lfe.is_some() { 4 } else { 3 };
1798        while self.mdct_states_multi.len() < n_channels {
1799            self.mdct_states_multi
1800                .push(EncoderMdctState::new(frame_len));
1801        }
1802        for state in self.mdct_states_multi.iter_mut() {
1803            if state.n != frame_len {
1804                *state = EncoderMdctState::new(frame_len);
1805            }
1806        }
1807        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
1808        for (ch, f) in frames.iter().enumerate() {
1809            let c = self.mdct_states_multi[ch].analyse_frame(f);
1810            coeffs_per_channel.push(c);
1811        }
1812        let coeffs_lfe: Option<Vec<f32>> =
1813            lfe.map(|buf| self.mdct_states_multi[3].analyse_frame(buf));
1814
1815        let aspx_cfg = crate::aspx::AspxConfig {
1816            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
1817            start_freq: 0,
1818            stop_freq: 0,
1819            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
1820            interpolation: false,
1821            preflat: false,
1822            limiter: false,
1823            noise_sbg: 0,
1824            num_env_bits_fixfix: 0,
1825            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
1826        };
1827
1828        let acpl_num_param_bands_id: u8 = 3;
1829        let acpl_qm0 = crate::acpl::AcplQuantMode::Fine;
1830        let acpl_qm1 = crate::acpl::AcplQuantMode::Fine;
1831
1832        let pad_target_bytes: usize = match max_sfb {
1833            0..=20 => 4096,
1834            21..=40 => 8192,
1835            41..=50 => 16384,
1836            _ => 32768,
1837        };
1838
1839        let body = crate::encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_alpha_beta(
1840            frame_len,
1841            max_sfb,
1842            max_sfb_lfe,
1843            self.b_iframe_global,
1844            &coeffs_per_channel[0],
1845            &coeffs_per_channel[1],
1846            coeffs_lfe.as_deref(),
1847            &aspx_cfg,
1848            acpl_num_param_bands_id,
1849            acpl_qm0,
1850            acpl_qm1,
1851            alpha_scale,
1852            beta_scale,
1853            pad_target_bytes,
1854        );
1855
1856        let mut bw = BitWriter::new();
1857        self.write_toc(&mut bw);
1858        bw.align_to_byte();
1859        let mut out = bw.finish();
1860        out.extend(body);
1861        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
1862        self.channel_mode_value = saved_mode.0;
1863        self.channel_mode_bits = saved_mode.1;
1864        out
1865    }
1866
1867    /// Encode one IMS v2 5.0 frame in 5_X_codec_mode = ASPX_ACPL_3 with
1868    /// **real per-band α + β + γ5/γ6**. Layered on top of
1869    /// [`Self::encode_frame_pcm_5_0_acpl3_real_alpha_beta`]: the γ5 / γ6
1870    /// entropy layers now carry per-band magnitudes derived from a 2×2
1871    /// least-squares fit of the centre channel
1872    /// `C ≈ K · (γ5·L + γ6·R)` (Pseudocode 118 step 7 + step 11 with
1873    /// `K = √2 · (1 + √2) / 2`). γ1..γ4 + β3 stay zero-delta.
1874    ///
1875    /// `gamma_scale = 0.0` reproduces the round-196 real-α-β byte stream
1876    /// exactly; `gamma_scale = 1.0` writes the full analytic γ pair
1877    /// (clamped to the Table-208 ±2.0 bound). `alpha_scale = beta_scale
1878    /// = gamma_scale = 0.0` reproduces the round-95 zero-delta scaffold
1879    /// byte-for-byte.
1880    ///
1881    /// `frames` is in `[L, R, C]` order. The decoder walks the same
1882    /// Table 25 ASPX_ACPL_3 body; γ5 / γ6 feed the ACplModule2 instance
1883    /// that synthesises the centre output channel (Pseudocode 119 with
1884    /// `a = 1`, `b = 0`, `y = 0`). γ1..γ4 still at zero-delta keeps the
1885    /// (L, R, Ls, Rs) sub-pipeline behaviour identical to the round-196
1886    /// path.
1887    pub fn encode_frame_pcm_5_0_acpl3_real_alpha_beta_gamma(
1888        &mut self,
1889        frames: &[&[f32]; 3],
1890        alpha_scale: f32,
1891        beta_scale: f32,
1892        gamma_scale: f32,
1893    ) -> Vec<u8> {
1894        self.encode_frame_pcm_5_x_acpl3_real_alpha_beta_gamma_with_max_sfb(
1895            frames,
1896            None,
1897            40,
1898            None,
1899            alpha_scale,
1900            beta_scale,
1901            gamma_scale,
1902        )
1903    }
1904
1905    /// 5.1 counterpart to
1906    /// [`Self::encode_frame_pcm_5_0_acpl3_real_alpha_beta_gamma`].
1907    /// `frames` is in `[L, R, C, LFE]` order. The LFE channel is coded
1908    /// as a leading `mono_data(b_lfe = 1)` element per Table 21 — same
1909    /// path as [`Self::encode_frame_pcm_5_1_acpl3_real_alpha_beta`].
1910    pub fn encode_frame_pcm_5_1_acpl3_real_alpha_beta_gamma(
1911        &mut self,
1912        frames: &[&[f32]; 4],
1913        alpha_scale: f32,
1914        beta_scale: f32,
1915        gamma_scale: f32,
1916    ) -> Vec<u8> {
1917        let carriers: [&[f32]; 3] = [frames[0], frames[1], frames[2]];
1918        self.encode_frame_pcm_5_x_acpl3_real_alpha_beta_gamma_with_max_sfb(
1919            &carriers,
1920            Some(frames[3]),
1921            40,
1922            Some(7),
1923            alpha_scale,
1924            beta_scale,
1925            gamma_scale,
1926        )
1927    }
1928
1929    /// Shared body for the real-α/β/γ5/γ6 ACPL_3 entry points. Mirrors
1930    /// [`Self::encode_frame_pcm_5_x_acpl3_real_alpha_beta_with_max_sfb`]
1931    /// but invokes the real-γ5/γ6 builder.
1932    #[allow(clippy::too_many_arguments)]
1933    fn encode_frame_pcm_5_x_acpl3_real_alpha_beta_gamma_with_max_sfb(
1934        &mut self,
1935        frames: &[&[f32]; 3],
1936        lfe: Option<&[f32]>,
1937        max_sfb: u32,
1938        max_sfb_lfe: Option<u32>,
1939        alpha_scale: f32,
1940        beta_scale: f32,
1941        gamma_scale: f32,
1942    ) -> Vec<u8> {
1943        let (_fps_milli, frame_len) =
1944            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
1945        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
1946        for (ch, f) in frames.iter().enumerate() {
1947            assert_eq!(
1948                f.len(),
1949                frame_len as usize,
1950                "encode_frame_pcm_5_x_acpl3_real_alpha_beta_gamma: channel {ch} input length must match frame_len = {frame_len}"
1951            );
1952        }
1953        if let Some(lfe_buf) = lfe {
1954            assert_eq!(
1955                lfe_buf.len(),
1956                frame_len as usize,
1957                "encode_frame_pcm_5_x_acpl3_real_alpha_beta_gamma: LFE input length must match frame_len = {frame_len}"
1958            );
1959        }
1960        let (n_msfb_bits, _, _) =
1961            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
1962        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
1963        let max_sfb = max_sfb.min(n_msfb_cap);
1964
1965        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
1966        if lfe.is_some() {
1967            self.channel_mode_value = 0b1110;
1968            self.channel_mode_bits = 4;
1969        } else {
1970            self.channel_mode_value = 0b1101;
1971            self.channel_mode_bits = 4;
1972        }
1973
1974        let n_channels = if lfe.is_some() { 4 } else { 3 };
1975        while self.mdct_states_multi.len() < n_channels {
1976            self.mdct_states_multi
1977                .push(EncoderMdctState::new(frame_len));
1978        }
1979        for state in self.mdct_states_multi.iter_mut() {
1980            if state.n != frame_len {
1981                *state = EncoderMdctState::new(frame_len);
1982            }
1983        }
1984        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
1985        for (ch, f) in frames.iter().enumerate() {
1986            let c = self.mdct_states_multi[ch].analyse_frame(f);
1987            coeffs_per_channel.push(c);
1988        }
1989        let coeffs_lfe: Option<Vec<f32>> =
1990            lfe.map(|buf| self.mdct_states_multi[3].analyse_frame(buf));
1991
1992        let aspx_cfg = crate::aspx::AspxConfig {
1993            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
1994            start_freq: 0,
1995            stop_freq: 0,
1996            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
1997            interpolation: false,
1998            preflat: false,
1999            limiter: false,
2000            noise_sbg: 0,
2001            num_env_bits_fixfix: 0,
2002            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
2003        };
2004
2005        let acpl_num_param_bands_id: u8 = 3;
2006        let acpl_qm0 = crate::acpl::AcplQuantMode::Fine;
2007        let acpl_qm1 = crate::acpl::AcplQuantMode::Fine;
2008
2009        let pad_target_bytes: usize = match max_sfb {
2010            0..=20 => 4096,
2011            21..=40 => 8192,
2012            41..=50 => 16384,
2013            _ => 32768,
2014        };
2015
2016        let body =
2017            crate::encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_alpha_beta_gamma(
2018                frame_len,
2019                max_sfb,
2020                max_sfb_lfe,
2021                self.b_iframe_global,
2022                &coeffs_per_channel[0],
2023                &coeffs_per_channel[1],
2024                Some(&coeffs_per_channel[2]),
2025                coeffs_lfe.as_deref(),
2026                &aspx_cfg,
2027                acpl_num_param_bands_id,
2028                acpl_qm0,
2029                acpl_qm1,
2030                alpha_scale,
2031                beta_scale,
2032                gamma_scale,
2033                pad_target_bytes,
2034            );
2035
2036        let mut bw = BitWriter::new();
2037        self.write_toc(&mut bw);
2038        bw.align_to_byte();
2039        let mut out = bw.finish();
2040        out.extend(body);
2041        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
2042        self.channel_mode_value = saved_mode.0;
2043        self.channel_mode_bits = saved_mode.1;
2044        out
2045    }
2046
2047    /// Encode one IMS v2 5.0 frame in 5_X_codec_mode = ASPX_ACPL_3 with
2048    /// **full** real per-parameter-band α₁ / α₂ / β₁ / β₂ / γ₁..γ₆
2049    /// extraction (round 215) — the round-208 entry point
2050    /// [`Self::encode_frame_pcm_5_0_acpl3_real_alpha_beta_gamma`] lifted
2051    /// the centre γ₅ / γ₆ pair to real values; this entry point also
2052    /// lifts γ₁ / γ₂ (driving the (L, Ls) pair via Pseudocode 118
2053    /// step 5) and γ₃ / γ₄ (driving the (R, Rs) pair via Pseudocode 118
2054    /// step 6), closing the README's long-standing "γ1..γ4 stay at the
2055    /// round-95 zero-delta scaffold" deferral for the 5_X ACPL_3 path.
2056    ///
2057    /// The γ₁ / γ₂ pair comes from a per-band 2×2 least-squares fit of
2058    /// the (L, Ls) output sum `(L + Ls/√2)/(1 + √2)` onto the (L, R)
2059    /// carrier pair; γ₃ / γ₄ come from the symmetric fit on the
2060    /// (R, Rs) pair (see
2061    /// [`crate::encoder_acpl3::extract_gamma_1_2_q_per_band_surround_least_squares`]
2062    /// and
2063    /// [`crate::encoder_acpl3::extract_gamma_3_4_q_per_band_surround_least_squares`]).
2064    /// Both sums are independent of the α / β decorrelator
2065    /// contributions and equal a `(1 + √2) · (γ·L + γ'·R)` linear
2066    /// combination, so the resulting 2×2 normal-equations system is
2067    /// identical in shape to the round-208 γ₅ / γ₆ centre fit.
2068    ///
2069    /// `frames` is in `[L, R, C, Ls, Rs]` order. The L / R carriers
2070    /// also feed the round-51 stereo `two_channel_data()` body the
2071    /// ASPX_ACPL_3 path emits. β₃ stays at the round-95 zero-delta
2072    /// scaffold — its analytic extraction requires a model for the
2073    /// third decorrelator output `y₂` which is not observable at
2074    /// encode time. The ASPX envelope layer also stays at the
2075    /// minimum-bit-cost FIXFIX num_env=1 scaffold pending the
2076    /// "real ASPX envelope coding" deferral elsewhere on the README.
2077    ///
2078    /// `alpha_scale` / `beta_scale` / `gamma_scale` control the
2079    /// extractor magnitude (typically `1.0` for the analytic
2080    /// least-squares solution; `0.0` reproduces the prior-round
2081    /// scaffold byte-for-byte at the corresponding layer position).
2082    /// In particular `α/β/γ_scale = 0.0` reproduces the round-95
2083    /// zero-delta scaffold ([`Self::encode_frame_pcm_5_0_acpl3`])
2084    /// byte-for-byte; `γ_scale = 0.0` reproduces the round-196
2085    /// real-α-β bytes exactly.
2086    pub fn encode_frame_pcm_5_0_acpl3_real_alpha_beta_full_gamma(
2087        &mut self,
2088        frames: &[&[f32]; 5],
2089        alpha_scale: f32,
2090        beta_scale: f32,
2091        gamma_scale: f32,
2092    ) -> Vec<u8> {
2093        self.encode_frame_pcm_5_x_acpl3_real_alpha_beta_full_gamma_with_max_sfb(
2094            frames,
2095            None,
2096            40,
2097            None,
2098            alpha_scale,
2099            beta_scale,
2100            gamma_scale,
2101        )
2102    }
2103
2104    /// 5.1 counterpart to
2105    /// [`Self::encode_frame_pcm_5_0_acpl3_real_alpha_beta_full_gamma`].
2106    /// `frames` is in `[L, R, C, Ls, Rs, LFE]` order. The LFE channel
2107    /// is coded as a leading `mono_data(b_lfe = 1)` element per Table
2108    /// 21 — same path as
2109    /// [`Self::encode_frame_pcm_5_1_acpl3_real_alpha_beta_gamma`].
2110    pub fn encode_frame_pcm_5_1_acpl3_real_alpha_beta_full_gamma(
2111        &mut self,
2112        frames: &[&[f32]; 6],
2113        alpha_scale: f32,
2114        beta_scale: f32,
2115        gamma_scale: f32,
2116    ) -> Vec<u8> {
2117        let surround: [&[f32]; 5] = [frames[0], frames[1], frames[2], frames[3], frames[4]];
2118        self.encode_frame_pcm_5_x_acpl3_real_alpha_beta_full_gamma_with_max_sfb(
2119            &surround,
2120            Some(frames[5]),
2121            40,
2122            Some(7),
2123            alpha_scale,
2124            beta_scale,
2125            gamma_scale,
2126        )
2127    }
2128
2129    /// Shared body for the real-α/β + full real-γ₁..γ₆ ACPL_3 entry
2130    /// points. Mirrors
2131    /// [`Self::encode_frame_pcm_5_x_acpl3_real_alpha_beta_gamma_with_max_sfb`]
2132    /// but accepts a 5-channel `[L, R, C, Ls, Rs]` input and invokes
2133    /// the
2134    /// [`crate::encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_alpha_beta_full_gamma`]
2135    /// builder.
2136    #[allow(clippy::too_many_arguments)]
2137    fn encode_frame_pcm_5_x_acpl3_real_alpha_beta_full_gamma_with_max_sfb(
2138        &mut self,
2139        frames: &[&[f32]; 5],
2140        lfe: Option<&[f32]>,
2141        max_sfb: u32,
2142        max_sfb_lfe: Option<u32>,
2143        alpha_scale: f32,
2144        beta_scale: f32,
2145        gamma_scale: f32,
2146    ) -> Vec<u8> {
2147        let (_fps_milli, frame_len) =
2148            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
2149        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
2150        for (ch, f) in frames.iter().enumerate() {
2151            assert_eq!(
2152                f.len(),
2153                frame_len as usize,
2154                "encode_frame_pcm_5_x_acpl3_real_alpha_beta_full_gamma: channel {ch} input length must match frame_len = {frame_len}"
2155            );
2156        }
2157        if let Some(lfe_buf) = lfe {
2158            assert_eq!(
2159                lfe_buf.len(),
2160                frame_len as usize,
2161                "encode_frame_pcm_5_x_acpl3_real_alpha_beta_full_gamma: LFE input length must match frame_len = {frame_len}"
2162            );
2163        }
2164        let (n_msfb_bits, _, _) =
2165            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
2166        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
2167        let max_sfb = max_sfb.min(n_msfb_cap);
2168
2169        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
2170        if lfe.is_some() {
2171            self.channel_mode_value = 0b1110;
2172            self.channel_mode_bits = 4;
2173        } else {
2174            self.channel_mode_value = 0b1101;
2175            self.channel_mode_bits = 4;
2176        }
2177
2178        let n_channels = if lfe.is_some() { 6 } else { 5 };
2179        while self.mdct_states_multi.len() < n_channels {
2180            self.mdct_states_multi
2181                .push(EncoderMdctState::new(frame_len));
2182        }
2183        for state in self.mdct_states_multi.iter_mut() {
2184            if state.n != frame_len {
2185                *state = EncoderMdctState::new(frame_len);
2186            }
2187        }
2188        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
2189        for (ch, f) in frames.iter().enumerate() {
2190            let c = self.mdct_states_multi[ch].analyse_frame(f);
2191            coeffs_per_channel.push(c);
2192        }
2193        let coeffs_lfe: Option<Vec<f32>> =
2194            lfe.map(|buf| self.mdct_states_multi[5].analyse_frame(buf));
2195
2196        let aspx_cfg = crate::aspx::AspxConfig {
2197            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
2198            start_freq: 0,
2199            stop_freq: 0,
2200            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
2201            interpolation: false,
2202            preflat: false,
2203            limiter: false,
2204            noise_sbg: 0,
2205            num_env_bits_fixfix: 0,
2206            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
2207        };
2208
2209        let acpl_num_param_bands_id: u8 = 3;
2210        let acpl_qm0 = crate::acpl::AcplQuantMode::Fine;
2211        let acpl_qm1 = crate::acpl::AcplQuantMode::Fine;
2212
2213        let pad_target_bytes: usize = match max_sfb {
2214            0..=20 => 4096,
2215            21..=40 => 8192,
2216            41..=50 => 16384,
2217            _ => 32768,
2218        };
2219
2220        let body =
2221            crate::encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_alpha_beta_full_gamma(
2222                frame_len,
2223                max_sfb,
2224                max_sfb_lfe,
2225                self.b_iframe_global,
2226                &coeffs_per_channel[0],
2227                &coeffs_per_channel[1],
2228                Some(&coeffs_per_channel[2]),
2229                Some(&coeffs_per_channel[3]),
2230                Some(&coeffs_per_channel[4]),
2231                coeffs_lfe.as_deref(),
2232                &aspx_cfg,
2233                acpl_num_param_bands_id,
2234                acpl_qm0,
2235                acpl_qm1,
2236                alpha_scale,
2237                beta_scale,
2238                gamma_scale,
2239                pad_target_bytes,
2240            );
2241
2242        let mut bw = BitWriter::new();
2243        self.write_toc(&mut bw);
2244        bw.align_to_byte();
2245        let mut out = bw.finish();
2246        out.extend(body);
2247        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
2248        self.channel_mode_value = saved_mode.0;
2249        self.channel_mode_bits = saved_mode.1;
2250        out
2251    }
2252
2253    /// Encode one IMS v2 5.0 frame in 5_X_codec_mode = ASPX_ACPL_3 with
2254    /// **real per-parameter-band α₁ / α₂ + β₁ / β₂ + γ₁..γ₆ + β₃**
2255    /// extraction (round 285 — the β₃-real extension of
2256    /// [`Self::encode_frame_pcm_5_0_acpl3_real_alpha_beta_full_gamma`]).
2257    ///
2258    /// `frames` is in `[L, R, C, Ls, Rs]` order. β₃ (the gain on the
2259    /// third decorrelator output `y₂` per §5.7.7.6.2 Pseudocode 118
2260    /// steps 8–10) is energy-matched to the centre-channel
2261    /// reconstruction residual left over after the γ₅ / γ₆ dry-mix fit
2262    /// — see
2263    /// [`crate::encoder_acpl3::extract_beta3_q_per_band_centre_residual`].
2264    /// `beta3_scale = 0.0` reproduces the round-215 full-γ byte stream
2265    /// exactly; `beta3_scale = 1.0` applies the full energy-matching
2266    /// solution clamped to the Table-207 ±1.0 magnitude bound.
2267    pub fn encode_frame_pcm_5_0_acpl3_real_alpha_beta_full_gamma_beta3(
2268        &mut self,
2269        frames: &[&[f32]; 5],
2270        alpha_scale: f32,
2271        beta_scale: f32,
2272        gamma_scale: f32,
2273        beta3_scale: f32,
2274    ) -> Vec<u8> {
2275        self.encode_frame_pcm_5_x_acpl3_real_alpha_beta_full_gamma_beta3_with_max_sfb(
2276            frames,
2277            None,
2278            40,
2279            None,
2280            alpha_scale,
2281            beta_scale,
2282            gamma_scale,
2283            beta3_scale,
2284        )
2285    }
2286
2287    /// 5.1 counterpart to
2288    /// [`Self::encode_frame_pcm_5_0_acpl3_real_alpha_beta_full_gamma_beta3`].
2289    /// `frames` is in `[L, R, C, Ls, Rs, LFE]` order. The LFE channel
2290    /// is coded as a leading `mono_data(b_lfe = 1)` element per Table
2291    /// 21 — same path as
2292    /// [`Self::encode_frame_pcm_5_1_acpl3_real_alpha_beta_full_gamma`].
2293    pub fn encode_frame_pcm_5_1_acpl3_real_alpha_beta_full_gamma_beta3(
2294        &mut self,
2295        frames: &[&[f32]; 6],
2296        alpha_scale: f32,
2297        beta_scale: f32,
2298        gamma_scale: f32,
2299        beta3_scale: f32,
2300    ) -> Vec<u8> {
2301        let surround: [&[f32]; 5] = [frames[0], frames[1], frames[2], frames[3], frames[4]];
2302        self.encode_frame_pcm_5_x_acpl3_real_alpha_beta_full_gamma_beta3_with_max_sfb(
2303            &surround,
2304            Some(frames[5]),
2305            40,
2306            Some(7),
2307            alpha_scale,
2308            beta_scale,
2309            gamma_scale,
2310            beta3_scale,
2311        )
2312    }
2313
2314    /// Shared body for the real-α/β/γ₁..γ₆/β₃ ACPL_3 entry points.
2315    /// Mirrors
2316    /// [`Self::encode_frame_pcm_5_x_acpl3_real_alpha_beta_full_gamma_with_max_sfb`]
2317    /// but invokes the
2318    /// [`crate::encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_alpha_beta_full_gamma_beta3`]
2319    /// builder with the additional `beta3_scale` decision knob.
2320    #[allow(clippy::too_many_arguments)]
2321    fn encode_frame_pcm_5_x_acpl3_real_alpha_beta_full_gamma_beta3_with_max_sfb(
2322        &mut self,
2323        frames: &[&[f32]; 5],
2324        lfe: Option<&[f32]>,
2325        max_sfb: u32,
2326        max_sfb_lfe: Option<u32>,
2327        alpha_scale: f32,
2328        beta_scale: f32,
2329        gamma_scale: f32,
2330        beta3_scale: f32,
2331    ) -> Vec<u8> {
2332        let (_fps_milli, frame_len) =
2333            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
2334        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
2335        for (ch, f) in frames.iter().enumerate() {
2336            assert_eq!(
2337                f.len(),
2338                frame_len as usize,
2339                "encode_frame_pcm_5_x_acpl3_real_alpha_beta_full_gamma_beta3: channel {ch} input length must match frame_len = {frame_len}"
2340            );
2341        }
2342        if let Some(lfe_buf) = lfe {
2343            assert_eq!(
2344                lfe_buf.len(),
2345                frame_len as usize,
2346                "encode_frame_pcm_5_x_acpl3_real_alpha_beta_full_gamma_beta3: LFE input length must match frame_len = {frame_len}"
2347            );
2348        }
2349        let (n_msfb_bits, _, _) =
2350            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
2351        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
2352        let max_sfb = max_sfb.min(n_msfb_cap);
2353
2354        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
2355        if lfe.is_some() {
2356            self.channel_mode_value = 0b1110;
2357            self.channel_mode_bits = 4;
2358        } else {
2359            self.channel_mode_value = 0b1101;
2360            self.channel_mode_bits = 4;
2361        }
2362
2363        let n_channels = if lfe.is_some() { 6 } else { 5 };
2364        while self.mdct_states_multi.len() < n_channels {
2365            self.mdct_states_multi
2366                .push(EncoderMdctState::new(frame_len));
2367        }
2368        for state in self.mdct_states_multi.iter_mut() {
2369            if state.n != frame_len {
2370                *state = EncoderMdctState::new(frame_len);
2371            }
2372        }
2373        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
2374        for (ch, f) in frames.iter().enumerate() {
2375            let c = self.mdct_states_multi[ch].analyse_frame(f);
2376            coeffs_per_channel.push(c);
2377        }
2378        let coeffs_lfe: Option<Vec<f32>> =
2379            lfe.map(|buf| self.mdct_states_multi[5].analyse_frame(buf));
2380
2381        let aspx_cfg = crate::aspx::AspxConfig {
2382            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
2383            start_freq: 0,
2384            stop_freq: 0,
2385            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
2386            interpolation: false,
2387            preflat: false,
2388            limiter: false,
2389            noise_sbg: 0,
2390            num_env_bits_fixfix: 0,
2391            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
2392        };
2393
2394        let acpl_num_param_bands_id: u8 = 3;
2395        let acpl_qm0 = crate::acpl::AcplQuantMode::Fine;
2396        let acpl_qm1 = crate::acpl::AcplQuantMode::Fine;
2397
2398        let pad_target_bytes: usize = match max_sfb {
2399            0..=20 => 4096,
2400            21..=40 => 8192,
2401            41..=50 => 16384,
2402            _ => 32768,
2403        };
2404
2405        let body =
2406            crate::encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_alpha_beta_full_gamma_beta3(
2407                frame_len,
2408                max_sfb,
2409                max_sfb_lfe,
2410                self.b_iframe_global,
2411                &coeffs_per_channel[0],
2412                &coeffs_per_channel[1],
2413                Some(&coeffs_per_channel[2]),
2414                Some(&coeffs_per_channel[3]),
2415                Some(&coeffs_per_channel[4]),
2416                coeffs_lfe.as_deref(),
2417                &aspx_cfg,
2418                acpl_num_param_bands_id,
2419                acpl_qm0,
2420                acpl_qm1,
2421                alpha_scale,
2422                beta_scale,
2423                gamma_scale,
2424                beta3_scale,
2425                pad_target_bytes,
2426            );
2427
2428        let mut bw = BitWriter::new();
2429        self.write_toc(&mut bw);
2430        bw.align_to_byte();
2431        let mut out = bw.finish();
2432        out.extend(body);
2433        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
2434        self.channel_mode_value = saved_mode.0;
2435        self.channel_mode_bits = saved_mode.1;
2436        out
2437    }
2438
2439    /// Encode one IMS v2 5.0 frame in 5_X_codec_mode = ASPX_ACPL_2 per
2440    /// ETSI TS 103 190-1 §4.2.6.6 Table 25 row `case ASPX_ACPL_2:`
2441    /// (round 100). Symmetric counterpart to the decoder's round-25
2442    /// [`crate::mch::parse_5x_audio_data_outer`] ASPX_ACPL_{1,2} inner-
2443    /// body walker (Pseudocode 117).
2444    ///
2445    /// `frames` is in `[L, R, C]` order — the L/R carrier pair feeds the
2446    /// `two_channel_data()` body and drives the A-CPL Ls/Rs surround
2447    /// reconstruction via [`crate::acpl_synth::run_acpl_5x_pair_pcm`] at
2448    /// decode time; the centre carrier `C` is coded as a Cfg0
2449    /// `mono_data(0)` element. ASPX_ACPL_2 has no surround carriers — the
2450    /// Ls/Rs PCM is reconstructed entirely from the L/R carriers + the
2451    /// two `acpl_data_1ch()` parameter sets.
2452    ///
2453    /// The encoder forces the 5.0 channel_mode prefix (`0b1101`, 4 b —
2454    /// Table 85 channel_mode 3) so the decoder's `walk_ac4_substream`
2455    /// dispatches `channels == 5` through
2456    /// `parse_5x_audio_data_outer(b_has_lfe = false)` with
2457    /// `5_X_codec_mode = AspxAcpl2`.
2458    ///
2459    /// The ASPX/A-CPL parameter bits are emitted as minimum-bit-cost
2460    /// zero-delta Huffman codewords (the round-95 "structural scaffold"
2461    /// mode — see [`crate::encoder_acpl3`]). The decoder walks the full
2462    /// Table 25 ASPX_ACPL_2 body and produces 5-channel `[L, R, C, Ls,
2463    /// Rs]` PCM. With all-zero ACPL parameter deltas the surround pair
2464    /// Ls/Rs collapses to the ducker-driven reconstruction from the L/R
2465    /// carriers.
2466    ///
2467    /// `max_sfb` defaults to 40 (matching the round-95 ACPL_3 default).
2468    pub fn encode_frame_pcm_5_0_acpl2(&mut self, frames: &[&[f32]; 3]) -> Vec<u8> {
2469        self.encode_frame_pcm_5_0_acpl2_with_max_sfb(frames, 40)
2470    }
2471
2472    /// `max_sfb`-parameterised form of [`Self::encode_frame_pcm_5_0_acpl2`].
2473    pub fn encode_frame_pcm_5_0_acpl2_with_max_sfb(
2474        &mut self,
2475        frames: &[&[f32]; 3],
2476        max_sfb: u32,
2477    ) -> Vec<u8> {
2478        let (_fps_milli, frame_len) =
2479            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
2480        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
2481        for (ch, f) in frames.iter().enumerate() {
2482            assert_eq!(
2483                f.len(),
2484                frame_len as usize,
2485                "encode_frame_pcm_5_0_acpl2: channel {ch} input length must match frame_len = {frame_len}"
2486            );
2487        }
2488        let (n_msfb_bits, _, _) =
2489            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
2490        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
2491        let max_sfb = max_sfb.min(n_msfb_cap);
2492
2493        // Force 5.0 channel_mode prefix '1101', 4 b → channel_mode 3.
2494        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
2495        self.channel_mode_value = 0b1101;
2496        self.channel_mode_bits = 4;
2497
2498        // Forward MDCT analysis per carrier channel (L, R, C — 3 states).
2499        let n_channels = 3;
2500        while self.mdct_states_multi.len() < n_channels {
2501            self.mdct_states_multi
2502                .push(EncoderMdctState::new(frame_len));
2503        }
2504        for state in self.mdct_states_multi.iter_mut() {
2505            if state.n != frame_len {
2506                *state = EncoderMdctState::new(frame_len);
2507            }
2508        }
2509        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
2510        for (ch, f) in frames.iter().enumerate() {
2511            let c = self.mdct_states_multi[ch].analyse_frame(f);
2512            coeffs_per_channel.push(c);
2513        }
2514
2515        // ASPX config: small low-res scale so the SBG counts stay small —
2516        // keeps the ASPX_data bodies compact. Matches the round-95
2517        // ASPX_ACPL_3 config exactly.
2518        let aspx_cfg = crate::aspx::AspxConfig {
2519            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
2520            start_freq: 0,
2521            stop_freq: 0,
2522            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
2523            interpolation: false,
2524            preflat: false,
2525            limiter: false,
2526            noise_sbg: 0, // num_noise_sbgroups = 1
2527            num_env_bits_fixfix: 0,
2528            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
2529        };
2530
2531        // ACPL: num_param_bands_id = 3 → 7 param bands; quant_mode Fine.
2532        let acpl_num_param_bands_id: u8 = 3;
2533        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
2534
2535        let pad_target_bytes: usize = match max_sfb {
2536            0..=20 => 4096,
2537            21..=40 => 8192,
2538            41..=50 => 16384,
2539            _ => 32768,
2540        };
2541
2542        let body = crate::encoder_acpl3::build_5_x_acpl2_body_from_pcm_spectra(
2543            frame_len,
2544            max_sfb,
2545            self.b_iframe_global,
2546            &coeffs_per_channel[0],
2547            &coeffs_per_channel[1],
2548            &coeffs_per_channel[2],
2549            &aspx_cfg,
2550            acpl_num_param_bands_id,
2551            acpl_quant_mode,
2552            pad_target_bytes,
2553        );
2554
2555        // Wrap in v2 IMS TOC.
2556        let mut bw = BitWriter::new();
2557        self.write_toc(&mut bw);
2558        bw.align_to_byte();
2559        let mut out = bw.finish();
2560        out.extend(body);
2561        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
2562        self.channel_mode_value = saved_mode.0;
2563        self.channel_mode_bits = saved_mode.1;
2564        out
2565    }
2566
2567    /// Encode one IMS v2 frame containing a 5.0 SIMPLE/ASPX_ACPL_2
2568    /// multichannel substream with **real per-parameter-band α + β
2569    /// extraction** carried by the two trailing `acpl_data_1ch()` elements
2570    /// (round 144 — the ACPL_2 5.0 counterpart to the round-132 ACPL_1 5.0
2571    /// real α + β path).
2572    ///
2573    /// Per ETSI TS 103 190-1 §5.7.7.5 Pseudocode 116 + §5.7.7.6.1
2574    /// Pseudocode 117, the A-CPL surround reconstruction carries the
2575    /// level component via α and a decorrelated residual via β:
2576    ///
2577    /// ```text
2578    ///   α   = 1 − 2·√2 · ⟨x_carrier, x_surround⟩ / ⟨x_carrier, x_carrier⟩
2579    ///   E[Ls²] = 0.5 · E[L²] · ( (1 − α)² + β² )
2580    ///   ⇒  β = √max(0, 2·E[Ls²]/E[L²] − (1 − α_dq)²)
2581    /// ```
2582    ///
2583    /// Unlike the ACPL_1 paths, ACPL_2 does **not** transmit the Ls/Rs
2584    /// surround pair on the wire — the decoder reconstructs the surround
2585    /// purely from the L/R carriers + the two `acpl_data_1ch()` parameter
2586    /// sets. This entry point therefore still emits the round-100
2587    /// ASPX_ACPL_2 body layout (no joint-MDCT residual layer, no
2588    /// `acpl_config_1ch(PARTIAL)` qmf_band field), but extracts the α + β
2589    /// indices from the caller's full 5-channel `[L, R, C, Ls, Rs]` input
2590    /// rather than pinning them at the zero-codebook scaffold.
2591    ///
2592    /// `frames` is in `[L, R, C, Ls, Rs]` order; β3 / γ stay at the
2593    /// scaffold. The `acpl_config_1ch(FULL)` carries no `qmf_band` →
2594    /// `start_band = 0` so every parameter band participates in the α + β
2595    /// coding (in contrast to the ACPL_1 PARTIAL mode whose
2596    /// `acpl_qmf_band` masks the low bands).
2597    ///
2598    /// **Note (round-128 ALPHA F0 writer-side `alpha_q` desync —
2599    /// deferred follow-up since round 132).** The shared
2600    /// `write_acpl_alpha_f0_value` writer treats the signed `alpha_q ∈
2601    /// [-N/2..+N/2]` returned by `quantise_alpha` as a raw F0 symbol
2602    /// index without re-centering it against the table's shortest
2603    /// codeword. The decoder's `dequantize_alpha_index` re-centers via
2604    /// `lane = alpha_q + N/2`, so non-trivial α values do not round-trip
2605    /// bit-exact through the full PCM→MDCT→writer→parser→synth chain
2606    /// when the analytic α resolves to a non-center quantisation lane.
2607    /// The on-wire β codewords for ACPL_2 are wired correctly per
2608    /// §A.3 Table A.40 / A.41 (β uses unsigned-magnitude F0 with
2609    /// `cb_off = 0`, no re-centering needed); the round-100 zero-α/β
2610    /// scaffold is structurally superseded by this entry point. Once
2611    /// the writer-side desync lands as a follow-up commit the on-wire β
2612    /// recovery will be bit-exact end-to-end.
2613    pub fn encode_frame_pcm_5_0_acpl2_real_alpha_beta(&mut self, frames: &[&[f32]; 5]) -> Vec<u8> {
2614        self.encode_frame_pcm_5_0_acpl2_real_alpha_beta_with_max_sfb(frames, 40)
2615    }
2616
2617    /// `max_sfb`-parameterised form of
2618    /// [`Self::encode_frame_pcm_5_0_acpl2_real_alpha_beta`].
2619    pub fn encode_frame_pcm_5_0_acpl2_real_alpha_beta_with_max_sfb(
2620        &mut self,
2621        frames: &[&[f32]; 5],
2622        max_sfb: u32,
2623    ) -> Vec<u8> {
2624        let (_fps_milli, frame_len) =
2625            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
2626        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
2627        for (ch, f) in frames.iter().enumerate() {
2628            assert_eq!(
2629                f.len(),
2630                frame_len as usize,
2631                "encode_frame_pcm_5_0_acpl2_real_alpha_beta: channel {ch} input length must match frame_len = {frame_len}"
2632            );
2633        }
2634        let (n_msfb_bits, _, _) =
2635            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
2636        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
2637        let max_sfb = max_sfb.min(n_msfb_cap);
2638
2639        // Force 5.0 channel_mode prefix '1101', 4 b → channel_mode 3.
2640        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
2641        self.channel_mode_value = 0b1101;
2642        self.channel_mode_bits = 4;
2643
2644        // Forward MDCT analysis per carrier channel (L, R, C, Ls, Rs — 5
2645        // states). The Ls/Rs spectra feed the α + β extractors only — they
2646        // are not emitted on the ACPL_2 wire (the decoder reconstructs the
2647        // surround from L/R + the two acpl_data_1ch parameter sets).
2648        let n_channels = 5;
2649        while self.mdct_states_multi.len() < n_channels {
2650            self.mdct_states_multi
2651                .push(EncoderMdctState::new(frame_len));
2652        }
2653        for state in self.mdct_states_multi.iter_mut() {
2654            if state.n != frame_len {
2655                *state = EncoderMdctState::new(frame_len);
2656            }
2657        }
2658        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
2659        for (ch, f) in frames.iter().enumerate() {
2660            let c = self.mdct_states_multi[ch].analyse_frame(f);
2661            coeffs_per_channel.push(c);
2662        }
2663
2664        // ASPX config: matches the round-95 / 100 / 103 ASPX_ACPL config.
2665        let aspx_cfg = crate::aspx::AspxConfig {
2666            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
2667            start_freq: 0,
2668            stop_freq: 0,
2669            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
2670            interpolation: false,
2671            preflat: false,
2672            limiter: false,
2673            noise_sbg: 0, // num_noise_sbgroups = 1
2674            num_env_bits_fixfix: 0,
2675            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
2676        };
2677
2678        // ACPL: num_param_bands_id = 3 → 7 param bands; quant_mode Fine.
2679        let acpl_num_param_bands_id: u8 = 3;
2680        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
2681
2682        let pad_target_bytes: usize = match max_sfb {
2683            0..=20 => 4096,
2684            21..=40 => 8192,
2685            41..=50 => 16384,
2686            _ => 32768,
2687        };
2688
2689        let body = crate::encoder_acpl3::build_5_x_acpl2_body_from_pcm_spectra_real_alpha_beta(
2690            frame_len,
2691            max_sfb,
2692            self.b_iframe_global,
2693            &coeffs_per_channel[0],
2694            &coeffs_per_channel[1],
2695            &coeffs_per_channel[2],
2696            &coeffs_per_channel[3],
2697            &coeffs_per_channel[4],
2698            &aspx_cfg,
2699            acpl_num_param_bands_id,
2700            acpl_quant_mode,
2701            pad_target_bytes,
2702        );
2703
2704        // Wrap in v2 IMS TOC.
2705        let mut bw = BitWriter::new();
2706        self.write_toc(&mut bw);
2707        bw.align_to_byte();
2708        let mut out = bw.finish();
2709        out.extend(body);
2710        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
2711        self.channel_mode_value = saved_mode.0;
2712        self.channel_mode_bits = saved_mode.1;
2713        out
2714    }
2715
2716    /// Encode one IMS v2 frame containing a 5.0 SIMPLE/ASPX_ACPL_1
2717    /// multichannel substream per ETSI TS 103 190-1 §4.2.6.6 Table 25 row
2718    /// `case ASPX_ACPL_1:` (Pseudocode 117).
2719    ///
2720    /// Unlike ASPX_ACPL_2 (which reconstructs the Ls/Rs surround pair
2721    /// purely from the L/R carriers + the two `acpl_data_1ch()` parameter
2722    /// sets), ASPX_ACPL_1 transmits the surround signal explicitly as a
2723    /// **joint-MDCT residual layer** (`max_sfb_master + 2× chparam_info +
2724    /// 2× sf_data(ASF)`) keyed by the `acpl_config_1ch(PARTIAL)` element's
2725    /// `acpl_qmf_band` field. It therefore accepts a full 5-channel
2726    /// `[L, R, C, Ls, Rs]` input: L/R become the `two_channel_data()`
2727    /// carriers, C the Cfg0 `mono_data(0)`, and Ls/Rs the residual pair
2728    /// (sSMP,3 / sSMP,4 per Table 181).
2729    ///
2730    /// The encoder forces the 5.0 channel_mode prefix (`0b1101`, 4 b —
2731    /// Table 85 channel_mode 3) so the decoder's `walk_ac4_substream`
2732    /// dispatches `channels == 5` through
2733    /// `parse_5x_audio_data_outer(b_has_lfe = false)` with
2734    /// `5_X_codec_mode = AspxAcpl1`. The ASPX/A-CPL parameter bits use the
2735    /// round-95 minimum-bit-cost zero-delta Huffman scaffold. The decoder
2736    /// walks the full Table 25 ASPX_ACPL_1 body — including the residual
2737    /// layer that IMDCTs into the Ls/Rs PCM carriers — and produces
2738    /// 5-channel `[L, R, C, Ls, Rs]` PCM via
2739    /// [`crate::acpl_synth::run_acpl_5x_pair_pcm`] (Pseudocode 117).
2740    ///
2741    /// `max_sfb` defaults to 40; `max_sfb_master` (the residual-layer band
2742    /// budget) defaults to 20.
2743    pub fn encode_frame_pcm_5_0_acpl1(&mut self, frames: &[&[f32]; 5]) -> Vec<u8> {
2744        self.encode_frame_pcm_5_0_acpl1_with_max_sfb(frames, 40, 20)
2745    }
2746
2747    /// `max_sfb` / `max_sfb_master`-parameterised form of
2748    /// [`Self::encode_frame_pcm_5_0_acpl1`].
2749    pub fn encode_frame_pcm_5_0_acpl1_with_max_sfb(
2750        &mut self,
2751        frames: &[&[f32]; 5],
2752        max_sfb: u32,
2753        max_sfb_master: u32,
2754    ) -> Vec<u8> {
2755        let (_fps_milli, frame_len) =
2756            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
2757        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
2758        for (ch, f) in frames.iter().enumerate() {
2759            assert_eq!(
2760                f.len(),
2761                frame_len as usize,
2762                "encode_frame_pcm_5_0_acpl1: channel {ch} input length must match frame_len = {frame_len}"
2763            );
2764        }
2765        let (n_msfb_bits, _, _) =
2766            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
2767        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
2768        let max_sfb = max_sfb.min(n_msfb_cap);
2769
2770        // Force 5.0 channel_mode prefix '1101', 4 b → channel_mode 3.
2771        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
2772        self.channel_mode_value = 0b1101;
2773        self.channel_mode_bits = 4;
2774
2775        // Forward MDCT analysis per carrier channel (L, R, C, Ls, Rs — 5
2776        // states).
2777        let n_channels = 5;
2778        while self.mdct_states_multi.len() < n_channels {
2779            self.mdct_states_multi
2780                .push(EncoderMdctState::new(frame_len));
2781        }
2782        for state in self.mdct_states_multi.iter_mut() {
2783            if state.n != frame_len {
2784                *state = EncoderMdctState::new(frame_len);
2785            }
2786        }
2787        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
2788        for (ch, f) in frames.iter().enumerate() {
2789            let c = self.mdct_states_multi[ch].analyse_frame(f);
2790            coeffs_per_channel.push(c);
2791        }
2792
2793        // ASPX config: small low-res scale so the SBG counts stay small —
2794        // keeps the ASPX_data bodies compact. Matches the round-95 / 100
2795        // ASPX_ACPL_3 / ACPL_2 config exactly.
2796        let aspx_cfg = crate::aspx::AspxConfig {
2797            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
2798            start_freq: 0,
2799            stop_freq: 0,
2800            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
2801            interpolation: false,
2802            preflat: false,
2803            limiter: false,
2804            noise_sbg: 0, // num_noise_sbgroups = 1
2805            num_env_bits_fixfix: 0,
2806            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
2807        };
2808
2809        // ACPL: num_param_bands_id = 3 → 7 param bands; quant_mode Fine;
2810        // acpl_qmf_band_minus1 = 0 → qmf_band = 1 (PARTIAL mode).
2811        let acpl_num_param_bands_id: u8 = 3;
2812        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
2813        let acpl_qmf_band_minus1: u8 = 0;
2814
2815        let pad_target_bytes: usize = match max_sfb {
2816            0..=20 => 4096,
2817            21..=40 => 8192,
2818            41..=50 => 16384,
2819            _ => 32768,
2820        };
2821
2822        let body = crate::encoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra(
2823            frame_len,
2824            max_sfb,
2825            max_sfb_master,
2826            self.b_iframe_global,
2827            &coeffs_per_channel[0],
2828            &coeffs_per_channel[1],
2829            &coeffs_per_channel[2],
2830            &coeffs_per_channel[3],
2831            &coeffs_per_channel[4],
2832            &aspx_cfg,
2833            acpl_num_param_bands_id,
2834            acpl_quant_mode,
2835            acpl_qmf_band_minus1,
2836            pad_target_bytes,
2837        );
2838
2839        // Wrap in v2 IMS TOC.
2840        let mut bw = BitWriter::new();
2841        self.write_toc(&mut bw);
2842        bw.align_to_byte();
2843        let mut out = bw.finish();
2844        out.extend(body);
2845        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
2846        self.channel_mode_value = saved_mode.0;
2847        self.channel_mode_bits = saved_mode.1;
2848        out
2849    }
2850
2851    /// Encode one IMS v2 frame containing a 5.0 SIMPLE/ASPX_ACPL_1
2852    /// multichannel substream whose joint-MDCT residual layer is
2853    /// **SAP-coded by decision** (round 279) — the automatic,
2854    /// decision-driven counterpart of [`Self::encode_frame_pcm_5_0_acpl1`].
2855    ///
2856    /// Per ETSI TS 103 190-1 §5.3.4.3.2 / Table 181 + §5.3.2 Pseudocode
2857    /// 59, the encoder runs the round-271
2858    /// [`crate::asf::select_alpha_q_for_pair`] least-squares decision per
2859    /// `(L, Ls)` / `(R, Rs)` target pair, materialises the SAP-coded
2860    /// `chparam_info()` rows via
2861    /// [`crate::asf::build_chparam_info_sap_data_from_alpha_q`] (falling
2862    /// back to the header-only `SapMode::None` row when no band
2863    /// benefits), and transmits the Table-181 matrix-input carriers
2864    /// `(sSMP_A, sSMP_B) = (M, ·)` plus the side prediction residual
2865    /// `(sSMP_3, sSMP_4) = (S − g·M, ·)` recovered through
2866    /// [`crate::asf::invert_sap_table_181`]. For a surround pair
2867    /// correlated with its front carrier the residual sf_data collapses
2868    /// to (near-)silence — the bits the identity path spends on the raw
2869    /// Ls/Rs spectra are saved while the decoder's
2870    /// `apply_sap_table_181` forward mix reproduces the same
2871    /// preliminaries.
2872    ///
2873    /// On-wire body layout is identical to
2874    /// [`Self::encode_frame_pcm_5_0_acpl1`]; when the decision picks no
2875    /// SAP band (e.g. `Ls = L`) the emitted frame is bit-for-bit
2876    /// identical to the identity-SAP path.
2877    pub fn encode_frame_pcm_5_0_acpl1_sap(&mut self, frames: &[&[f32]; 5]) -> Vec<u8> {
2878        self.encode_frame_pcm_5_0_acpl1_sap_with_max_sfb(frames, 40, 20)
2879    }
2880
2881    /// `max_sfb` / `max_sfb_master`-parameterised form of
2882    /// [`Self::encode_frame_pcm_5_0_acpl1_sap`].
2883    pub fn encode_frame_pcm_5_0_acpl1_sap_with_max_sfb(
2884        &mut self,
2885        frames: &[&[f32]; 5],
2886        max_sfb: u32,
2887        max_sfb_master: u32,
2888    ) -> Vec<u8> {
2889        let (_fps_milli, frame_len) =
2890            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
2891        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
2892        for (ch, f) in frames.iter().enumerate() {
2893            assert_eq!(
2894                f.len(),
2895                frame_len as usize,
2896                "encode_frame_pcm_5_0_acpl1_sap: channel {ch} input length must match frame_len = {frame_len}"
2897            );
2898        }
2899        let (n_msfb_bits, _, _) =
2900            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
2901        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
2902        let max_sfb = max_sfb.min(n_msfb_cap);
2903
2904        // Force 5.0 channel_mode prefix '1101', 4 b → channel_mode 3.
2905        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
2906        self.channel_mode_value = 0b1101;
2907        self.channel_mode_bits = 4;
2908
2909        // Forward MDCT analysis per carrier channel (L, R, C, Ls, Rs).
2910        let n_channels = 5;
2911        while self.mdct_states_multi.len() < n_channels {
2912            self.mdct_states_multi
2913                .push(EncoderMdctState::new(frame_len));
2914        }
2915        for state in self.mdct_states_multi.iter_mut() {
2916            if state.n != frame_len {
2917                *state = EncoderMdctState::new(frame_len);
2918            }
2919        }
2920        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
2921        for (ch, f) in frames.iter().enumerate() {
2922            let c = self.mdct_states_multi[ch].analyse_frame(f);
2923            coeffs_per_channel.push(c);
2924        }
2925
2926        // Same ASPX / ACPL parameterisation as the round-103 path.
2927        let aspx_cfg = crate::aspx::AspxConfig {
2928            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
2929            start_freq: 0,
2930            stop_freq: 0,
2931            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
2932            interpolation: false,
2933            preflat: false,
2934            limiter: false,
2935            noise_sbg: 0,
2936            num_env_bits_fixfix: 0,
2937            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
2938        };
2939        let acpl_num_param_bands_id: u8 = 3;
2940        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
2941        let acpl_qmf_band_minus1: u8 = 0;
2942
2943        let pad_target_bytes: usize = match max_sfb {
2944            0..=20 => 4096,
2945            21..=40 => 8192,
2946            41..=50 => 16384,
2947            _ => 32768,
2948        };
2949
2950        let body = crate::encoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra_sap_auto(
2951            frame_len,
2952            max_sfb,
2953            max_sfb_master,
2954            self.b_iframe_global,
2955            &coeffs_per_channel[0],
2956            &coeffs_per_channel[1],
2957            &coeffs_per_channel[2],
2958            &coeffs_per_channel[3],
2959            &coeffs_per_channel[4],
2960            &aspx_cfg,
2961            acpl_num_param_bands_id,
2962            acpl_quant_mode,
2963            acpl_qmf_band_minus1,
2964            pad_target_bytes,
2965        );
2966
2967        // Wrap in v2 IMS TOC.
2968        let mut bw = BitWriter::new();
2969        self.write_toc(&mut bw);
2970        bw.align_to_byte();
2971        let mut out = bw.finish();
2972        out.extend(body);
2973        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
2974        self.channel_mode_value = saved_mode.0;
2975        self.channel_mode_bits = saved_mode.1;
2976        out
2977    }
2978
2979    /// Encode one IMS v2 frame containing a 5.0 SIMPLE/ASPX_ACPL_1
2980    /// multichannel substream with **real per-parameter-band α extraction**
2981    /// (round 128 — replaces the round-103 zero-delta scaffold for the
2982    /// α coefficient family; β / β3 / γ stay at the scaffold).
2983    ///
2984    /// Body layout is identical to [`Self::encode_frame_pcm_5_0_acpl1`]
2985    /// (delegates to [`crate::encoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra_real_alpha`]);
2986    /// the only on-wire difference is that the two `acpl_data_1ch()`
2987    /// elements now emit ALPHA F0 + DF codewords with non-zero values
2988    /// chosen to minimise the per-parameter-band residual against the
2989    /// caller's (Ls, Rs) input vs (L, R) carrier energies. See
2990    /// [`crate::encoder_acpl3`] §"Real per-band α extraction" for the
2991    /// closed-form derivation (β = 0 ⇒ α = 1 − 2·√2·⟨carrier, surround⟩
2992    /// / ⟨carrier, carrier⟩).
2993    ///
2994    /// `frames` is in `[L, R, C, Ls, Rs]` order. The decoder's
2995    /// [`crate::acpl_synth::run_acpl_5x_pair_pcm`] consumes the recovered
2996    /// α and reconstructs Ls / Rs from the L / R carriers with measurably
2997    /// better fidelity than the zero-α baseline when Ls / Rs aren't a
2998    /// pure scaled copy of L / R.
2999    pub fn encode_frame_pcm_5_0_acpl1_real_alpha(&mut self, frames: &[&[f32]; 5]) -> Vec<u8> {
3000        self.encode_frame_pcm_5_0_acpl1_real_alpha_with_max_sfb(frames, 40, 20)
3001    }
3002
3003    /// `max_sfb` / `max_sfb_master`-parameterised form of
3004    /// [`Self::encode_frame_pcm_5_0_acpl1_real_alpha`].
3005    pub fn encode_frame_pcm_5_0_acpl1_real_alpha_with_max_sfb(
3006        &mut self,
3007        frames: &[&[f32]; 5],
3008        max_sfb: u32,
3009        max_sfb_master: u32,
3010    ) -> Vec<u8> {
3011        let (_fps_milli, frame_len) =
3012            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
3013        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
3014        for (ch, f) in frames.iter().enumerate() {
3015            assert_eq!(
3016                f.len(),
3017                frame_len as usize,
3018                "encode_frame_pcm_5_0_acpl1_real_alpha: channel {ch} input length must match frame_len = {frame_len}"
3019            );
3020        }
3021        let (n_msfb_bits, _, _) =
3022            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
3023        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
3024        let max_sfb = max_sfb.min(n_msfb_cap);
3025
3026        // Force 5.0 channel_mode prefix '1101', 4 b → channel_mode 3.
3027        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
3028        self.channel_mode_value = 0b1101;
3029        self.channel_mode_bits = 4;
3030
3031        let n_channels = 5;
3032        while self.mdct_states_multi.len() < n_channels {
3033            self.mdct_states_multi
3034                .push(EncoderMdctState::new(frame_len));
3035        }
3036        for state in self.mdct_states_multi.iter_mut() {
3037            if state.n != frame_len {
3038                *state = EncoderMdctState::new(frame_len);
3039            }
3040        }
3041        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
3042        for (ch, f) in frames.iter().enumerate() {
3043            let c = self.mdct_states_multi[ch].analyse_frame(f);
3044            coeffs_per_channel.push(c);
3045        }
3046
3047        let aspx_cfg = crate::aspx::AspxConfig {
3048            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
3049            start_freq: 0,
3050            stop_freq: 0,
3051            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
3052            interpolation: false,
3053            preflat: false,
3054            limiter: false,
3055            noise_sbg: 0,
3056            num_env_bits_fixfix: 0,
3057            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
3058        };
3059
3060        let acpl_num_param_bands_id: u8 = 3;
3061        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
3062        let acpl_qmf_band_minus1: u8 = 0;
3063
3064        let pad_target_bytes: usize = match max_sfb {
3065            0..=20 => 4096,
3066            21..=40 => 8192,
3067            41..=50 => 16384,
3068            _ => 32768,
3069        };
3070
3071        let body = crate::encoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra_real_alpha(
3072            frame_len,
3073            max_sfb,
3074            max_sfb_master,
3075            self.b_iframe_global,
3076            &coeffs_per_channel[0],
3077            &coeffs_per_channel[1],
3078            &coeffs_per_channel[2],
3079            &coeffs_per_channel[3],
3080            &coeffs_per_channel[4],
3081            &aspx_cfg,
3082            acpl_num_param_bands_id,
3083            acpl_quant_mode,
3084            acpl_qmf_band_minus1,
3085            pad_target_bytes,
3086        );
3087
3088        let mut bw = BitWriter::new();
3089        self.write_toc(&mut bw);
3090        bw.align_to_byte();
3091        let mut out = bw.finish();
3092        out.extend(body);
3093        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
3094        self.channel_mode_value = saved_mode.0;
3095        self.channel_mode_bits = saved_mode.1;
3096        out
3097    }
3098
3099    /// Encode one IMS v2 frame containing a 5.0 SIMPLE/ASPX_ACPL_1
3100    /// multichannel substream with **real per-parameter-band α + β
3101    /// extraction** per ETSI TS 103 190-1 §5.7.7.5 Pseudocode 116 +
3102    /// §5.7.7.6.1 Pseudocode 117 (round 132).
3103    ///
3104    /// Extends [`Self::encode_frame_pcm_5_0_acpl1_real_alpha`] by emitting
3105    /// real per-band β magnitudes alongside the existing real α — the
3106    /// surround Ls/Rs reconstruction at the decoder is no longer a pure
3107    /// level-only image of L/R but also carries the energy of the
3108    /// decorrelated residual:
3109    ///
3110    /// ```text
3111    ///   E[Ls²] = 0.5 · E[L²] · ( (1 − α)² + β² )
3112    /// ```
3113    ///
3114    /// `frames` is in `[L, R, C, Ls, Rs]` order; β / γ stay at the
3115    /// round-95 / 100 / 103 / 128 scaffold for non-ACPL_1 paths.
3116    pub fn encode_frame_pcm_5_0_acpl1_real_alpha_beta(&mut self, frames: &[&[f32]; 5]) -> Vec<u8> {
3117        self.encode_frame_pcm_5_0_acpl1_real_alpha_beta_with_max_sfb(frames, 40, 20)
3118    }
3119
3120    /// `max_sfb` / `max_sfb_master`-parameterised form of
3121    /// [`Self::encode_frame_pcm_5_0_acpl1_real_alpha_beta`].
3122    pub fn encode_frame_pcm_5_0_acpl1_real_alpha_beta_with_max_sfb(
3123        &mut self,
3124        frames: &[&[f32]; 5],
3125        max_sfb: u32,
3126        max_sfb_master: u32,
3127    ) -> Vec<u8> {
3128        let (_fps_milli, frame_len) =
3129            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
3130        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
3131        for (ch, f) in frames.iter().enumerate() {
3132            assert_eq!(
3133                f.len(),
3134                frame_len as usize,
3135                "encode_frame_pcm_5_0_acpl1_real_alpha_beta: channel {ch} input length must match frame_len = {frame_len}"
3136            );
3137        }
3138        let (n_msfb_bits, _, _) =
3139            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
3140        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
3141        let max_sfb = max_sfb.min(n_msfb_cap);
3142
3143        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
3144        self.channel_mode_value = 0b1101;
3145        self.channel_mode_bits = 4;
3146
3147        let n_channels = 5;
3148        while self.mdct_states_multi.len() < n_channels {
3149            self.mdct_states_multi
3150                .push(EncoderMdctState::new(frame_len));
3151        }
3152        for state in self.mdct_states_multi.iter_mut() {
3153            if state.n != frame_len {
3154                *state = EncoderMdctState::new(frame_len);
3155            }
3156        }
3157        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
3158        for (ch, f) in frames.iter().enumerate() {
3159            let c = self.mdct_states_multi[ch].analyse_frame(f);
3160            coeffs_per_channel.push(c);
3161        }
3162
3163        let aspx_cfg = crate::aspx::AspxConfig {
3164            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
3165            start_freq: 0,
3166            stop_freq: 0,
3167            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
3168            interpolation: false,
3169            preflat: false,
3170            limiter: false,
3171            noise_sbg: 0,
3172            num_env_bits_fixfix: 0,
3173            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
3174        };
3175
3176        let acpl_num_param_bands_id: u8 = 3;
3177        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
3178        let acpl_qmf_band_minus1: u8 = 0;
3179
3180        let pad_target_bytes: usize = match max_sfb {
3181            0..=20 => 4096,
3182            21..=40 => 8192,
3183            41..=50 => 16384,
3184            _ => 32768,
3185        };
3186
3187        let body = crate::encoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra_real_alpha_beta(
3188            frame_len,
3189            max_sfb,
3190            max_sfb_master,
3191            self.b_iframe_global,
3192            &coeffs_per_channel[0],
3193            &coeffs_per_channel[1],
3194            &coeffs_per_channel[2],
3195            &coeffs_per_channel[3],
3196            &coeffs_per_channel[4],
3197            &aspx_cfg,
3198            acpl_num_param_bands_id,
3199            acpl_quant_mode,
3200            acpl_qmf_band_minus1,
3201            pad_target_bytes,
3202        );
3203
3204        let mut bw = BitWriter::new();
3205        self.write_toc(&mut bw);
3206        bw.align_to_byte();
3207        let mut out = bw.finish();
3208        out.extend(body);
3209        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
3210        self.channel_mode_value = saved_mode.0;
3211        self.channel_mode_bits = saved_mode.1;
3212        out
3213    }
3214
3215    /// Encode one IMS v2 frame containing a 7.0 SIMPLE/ASPX_ACPL_2
3216    /// multichannel substream per ETSI TS 103 190-1 §4.2.6.14 Table 33 row
3217    /// `case ASPX_ACPL_2:` (round 107). The 7_X (immersive) symmetric
3218    /// counterpart to the round-100 5_X ASPX_ACPL_2 encoder — it reuses the
3219    /// same 1ch ACPL / ASPX parameter shape (Pseudocode 117) but emits the
3220    /// 7_X channel element's distinct framing (2-bit `7_X_codec_mode`,
3221    /// `companding_control(5)`, 2-bit `coding_config`, two `two_channel_data`
3222    /// pairs, trailing centre `mono_data(0)`, and the two-`aspx_data_2ch`
3223    /// envelope trailer).
3224    ///
3225    /// `frames` is in `[L, R, C, Ls, Rs, Lb, Rb]` order — the 7.0 (3/4/0)
3226    /// surface layout. The L/R pair feeds the first `two_channel_data()`
3227    /// carriers and drives the A-CPL Ls/Rs surround reconstruction via
3228    /// [`crate::acpl_synth::run_acpl_5x_pair_pcm`] at decode time. The Ls/Rs
3229    /// pair is coded as the second `two_channel_data()` (keeps the body
3230    /// well-formed for the walker; the ACPL_2 dispatch reconstructs the
3231    /// surround from L/R + params). The centre `C` is the trailing Cfg0
3232    /// `mono_data(0)`. The back pair `Lb, Rb` is accepted for layout
3233    /// completeness but not carried by the ASPX_ACPL_2 body (the decoder's
3234    /// ACPL_2 7_X dispatch populates slots 0..4 only — slots 5/6 stay
3235    /// silent), matching the decoder's documented Table 202 channel mapping.
3236    ///
3237    /// The encoder forces the 7.0 (3/4/0) channel_mode prefix (`0b1111000`,
3238    /// 7 b — Table 85 channel_mode 5) so the decoder's `walk_ac4_substream`
3239    /// dispatches `channels == 7` through
3240    /// `parse_7x_audio_data_outer(b_has_lfe = false)` with
3241    /// `7_X_codec_mode = AspxAcpl2`. The ASPX/A-CPL parameter bits use the
3242    /// round-95 minimum-bit-cost zero-delta Huffman scaffold.
3243    ///
3244    /// `max_sfb` defaults to 40.
3245    pub fn encode_frame_pcm_7_0_acpl2(&mut self, frames: &[&[f32]; 7]) -> Vec<u8> {
3246        self.encode_frame_pcm_7_0_acpl2_with_max_sfb(frames, 40)
3247    }
3248
3249    /// `max_sfb`-parameterised form of [`Self::encode_frame_pcm_7_0_acpl2`].
3250    pub fn encode_frame_pcm_7_0_acpl2_with_max_sfb(
3251        &mut self,
3252        frames: &[&[f32]; 7],
3253        max_sfb: u32,
3254    ) -> Vec<u8> {
3255        let (_fps_milli, frame_len) =
3256            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
3257        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
3258        for (ch, f) in frames.iter().enumerate() {
3259            assert_eq!(
3260                f.len(),
3261                frame_len as usize,
3262                "encode_frame_pcm_7_0_acpl2: channel {ch} input length must match frame_len = {frame_len}"
3263            );
3264        }
3265        let (n_msfb_bits, _, _) =
3266            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
3267        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
3268        let max_sfb = max_sfb.min(n_msfb_cap);
3269
3270        // Force 7.0 (3/4/0) channel_mode prefix '1111000', 7 b →
3271        // channel_mode 5 (Table 85). The decoder routes channels == 7
3272        // through parse_7x_audio_data_outer(b_has_lfe = false).
3273        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
3274        self.channel_mode_value = 0b1111000;
3275        self.channel_mode_bits = 7;
3276
3277        // Forward MDCT analysis per channel — seven SCE states (L, R, C,
3278        // Ls, Rs, Lb, Rb). Only the first five feed the ASPX_ACPL_2 body;
3279        // the back pair is analysed for state continuity but its spectra
3280        // are not carried by the ACPL_2 path.
3281        let n_channels = 7;
3282        while self.mdct_states_multi.len() < n_channels {
3283            self.mdct_states_multi
3284                .push(EncoderMdctState::new(frame_len));
3285        }
3286        for state in self.mdct_states_multi.iter_mut() {
3287            if state.n != frame_len {
3288                *state = EncoderMdctState::new(frame_len);
3289            }
3290        }
3291        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
3292        for (ch, f) in frames.iter().enumerate() {
3293            let c = self.mdct_states_multi[ch].analyse_frame(f);
3294            coeffs_per_channel.push(c);
3295        }
3296
3297        // ASPX config: small low-res scale so the SBG counts stay small —
3298        // matches the round-95 / 100 / 103 ASPX_ACPL config exactly.
3299        let aspx_cfg = crate::aspx::AspxConfig {
3300            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
3301            start_freq: 0,
3302            stop_freq: 0,
3303            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
3304            interpolation: false,
3305            preflat: false,
3306            limiter: false,
3307            noise_sbg: 0, // num_noise_sbgroups = 1
3308            num_env_bits_fixfix: 0,
3309            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
3310        };
3311
3312        // ACPL: num_param_bands_id = 3 → 7 param bands; quant_mode Fine.
3313        let acpl_num_param_bands_id: u8 = 3;
3314        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
3315
3316        let pad_target_bytes: usize = match max_sfb {
3317            0..=20 => 4096,
3318            21..=40 => 12288,
3319            41..=50 => 24576,
3320            _ => 32767,
3321        };
3322
3323        let body = crate::encoder_acpl3::build_7_x_acpl2_body_from_pcm_spectra(
3324            frame_len,
3325            max_sfb,
3326            None, // 7.0 — no LFE
3327            self.b_iframe_global,
3328            &coeffs_per_channel[0],
3329            &coeffs_per_channel[1],
3330            &coeffs_per_channel[3],
3331            &coeffs_per_channel[4],
3332            &coeffs_per_channel[2],
3333            None, // 7.0 — no LFE
3334            &aspx_cfg,
3335            acpl_num_param_bands_id,
3336            acpl_quant_mode,
3337            pad_target_bytes,
3338        );
3339
3340        // Wrap in v2 IMS TOC.
3341        let mut bw = BitWriter::new();
3342        self.write_toc(&mut bw);
3343        bw.align_to_byte();
3344        let mut out = bw.finish();
3345        out.extend(body);
3346        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
3347        self.channel_mode_value = saved_mode.0;
3348        self.channel_mode_bits = saved_mode.1;
3349        out
3350    }
3351
3352    /// Encode one IMS v2 frame containing a 7.1 (3/4/0.1) SIMPLE/ASPX_ACPL_2
3353    /// multichannel substream per ETSI TS 103 190-1 §4.2.6.14 Table 33 row
3354    /// `case ASPX_ACPL_2:` with `b_has_lfe = 1` (round 114). The LFE
3355    /// counterpart of [`Self::encode_frame_pcm_7_0_acpl2`] — it emits the
3356    /// identical 7_X ASPX_ACPL_2 body plus a leading `mono_data(b_lfe = 1)`
3357    /// element (Table 21 + `sf_info_lfe()` Table 35) between the I-frame
3358    /// config block and `companding_control(5)`, exactly where the decoder's
3359    /// `parse_7x_audio_data_outer(b_has_lfe = true)` reads
3360    /// `if (b_has_lfe) mono_data(1);`.
3361    ///
3362    /// `frames` is in `[L, R, C, Ls, Rs, Lb, Rb, LFE]` order — the 7.1
3363    /// (3/4/0.1) surface layout. The L/R pair feeds the first
3364    /// `two_channel_data()` carriers and drives the A-CPL Ls/Rs surround
3365    /// reconstruction via [`crate::acpl_synth::run_acpl_5x_pair_pcm`] at
3366    /// decode time; the Ls/Rs pair rides the second `two_channel_data()`;
3367    /// the centre `C` is the trailing Cfg0 `mono_data(0)`; the LFE is the
3368    /// leading `mono_data(1)`. The back pair `Lb, Rb` is accepted for layout
3369    /// completeness but not carried by the ASPX_ACPL_2 body (the decoder's
3370    /// 7_X ACPL_2 dispatch populates slots 0..4 + the LFE slot 7 — slots 5/6
3371    /// stay silent), matching the round-107 documented Table 202 channel
3372    /// mapping plus the round-80 LFE PCM render at decode time.
3373    ///
3374    /// The encoder forces the 7.1 channel_mode prefix (`0b1111001`, 7 b —
3375    /// Table 88 channel_mode 6) so the decoder's `walk_ac4_substream`
3376    /// dispatches `channels == 8` through
3377    /// `parse_7x_audio_data_outer(b_has_lfe = true)` with
3378    /// `7_X_codec_mode = AspxAcpl2`. The ASPX/A-CPL parameter bits use the
3379    /// round-95 minimum-bit-cost zero-delta Huffman scaffold.
3380    ///
3381    /// `max_sfb` defaults to 40; `max_sfb_lfe` defaults to 7 (the LFE-spec
3382    /// cap at `tl = 1920`, `n_msfbl_bits = 3`).
3383    pub fn encode_frame_pcm_7_1_acpl2(&mut self, frames: &[&[f32]; 8]) -> Vec<u8> {
3384        self.encode_frame_pcm_7_1_acpl2_with_max_sfb(frames, 40, 7)
3385    }
3386
3387    /// `max_sfb`-parameterised form of [`Self::encode_frame_pcm_7_1_acpl2`].
3388    /// `max_sfb` governs the five front/surround carrier SCEs and the centre
3389    /// mono; `max_sfb_lfe` governs the LFE `mono_data(1)` (clamped to the
3390    /// `n_msfbl_bits` cap).
3391    pub fn encode_frame_pcm_7_1_acpl2_with_max_sfb(
3392        &mut self,
3393        frames: &[&[f32]; 8],
3394        max_sfb: u32,
3395        max_sfb_lfe: u32,
3396    ) -> Vec<u8> {
3397        let (_fps_milli, frame_len) =
3398            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
3399        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
3400        for (ch, f) in frames.iter().enumerate() {
3401            assert_eq!(
3402                f.len(),
3403                frame_len as usize,
3404                "encode_frame_pcm_7_1_acpl2: channel {ch} input length must match frame_len = {frame_len}"
3405            );
3406        }
3407        let (n_msfb_bits, _, n_msfbl_bits) =
3408            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
3409        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
3410        let max_sfb = max_sfb.min(n_msfb_cap);
3411        assert!(
3412            n_msfbl_bits > 0,
3413            "encode_frame_pcm_7_1_acpl2: tl = {frame_len} not permitted for LFE"
3414        );
3415        let n_msfbl_cap = (1u32 << n_msfbl_bits) - 1;
3416        let max_sfb_lfe = max_sfb_lfe.min(n_msfbl_cap);
3417
3418        // Force 7.1 (3/4/0.1) channel_mode prefix '1111001', 7 b →
3419        // channel_mode 6 (Table 88). The decoder routes channels == 8
3420        // through parse_7x_audio_data_outer(b_has_lfe = true).
3421        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
3422        self.channel_mode_value = 0b1111001;
3423        self.channel_mode_bits = 7;
3424
3425        // Forward MDCT analysis per channel — eight SCE states (L, R, C,
3426        // Ls, Rs, Lb, Rb, LFE). The first five + LFE feed the ASPX_ACPL_2
3427        // body; the back pair is analysed for state continuity but its
3428        // spectra are not carried by the ACPL_2 path.
3429        let n_channels = 8;
3430        while self.mdct_states_multi.len() < n_channels {
3431            self.mdct_states_multi
3432                .push(EncoderMdctState::new(frame_len));
3433        }
3434        for state in self.mdct_states_multi.iter_mut() {
3435            if state.n != frame_len {
3436                *state = EncoderMdctState::new(frame_len);
3437            }
3438        }
3439        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
3440        for (ch, f) in frames.iter().enumerate() {
3441            let c = self.mdct_states_multi[ch].analyse_frame(f);
3442            coeffs_per_channel.push(c);
3443        }
3444
3445        // ASPX config: matches the round-95 / 100 / 103 / 107 ASPX_ACPL
3446        // config exactly (small low-res scale → small SBG counts).
3447        let aspx_cfg = crate::aspx::AspxConfig {
3448            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
3449            start_freq: 0,
3450            stop_freq: 0,
3451            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
3452            interpolation: false,
3453            preflat: false,
3454            limiter: false,
3455            noise_sbg: 0, // num_noise_sbgroups = 1
3456            num_env_bits_fixfix: 0,
3457            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
3458        };
3459
3460        // ACPL: num_param_bands_id = 3 → 7 param bands; quant_mode Fine.
3461        let acpl_num_param_bands_id: u8 = 3;
3462        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
3463
3464        let pad_target_bytes: usize = match max_sfb {
3465            0..=20 => 4096,
3466            21..=40 => 12288,
3467            41..=50 => 24576,
3468            _ => 32767,
3469        };
3470
3471        let body = crate::encoder_acpl3::build_7_x_acpl2_body_from_pcm_spectra(
3472            frame_len,
3473            max_sfb,
3474            Some(max_sfb_lfe),
3475            self.b_iframe_global,
3476            &coeffs_per_channel[0],
3477            &coeffs_per_channel[1],
3478            &coeffs_per_channel[3],
3479            &coeffs_per_channel[4],
3480            &coeffs_per_channel[2],
3481            Some(&coeffs_per_channel[7]),
3482            &aspx_cfg,
3483            acpl_num_param_bands_id,
3484            acpl_quant_mode,
3485            pad_target_bytes,
3486        );
3487
3488        // Wrap in v2 IMS TOC.
3489        let mut bw = BitWriter::new();
3490        self.write_toc(&mut bw);
3491        bw.align_to_byte();
3492        let mut out = bw.finish();
3493        out.extend(body);
3494        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
3495        self.channel_mode_value = saved_mode.0;
3496        self.channel_mode_bits = saved_mode.1;
3497        out
3498    }
3499
3500    /// Encode one IMS v2 frame containing a 7.0 (3/4/0) SIMPLE/ASPX_ACPL_2
3501    /// multichannel substream per ETSI TS 103 190-1 §4.2.6.14 Table 33 row
3502    /// `case ASPX_ACPL_2:` with **real per-parameter-band α + β extraction**
3503    /// (round 202). The 7_X (immersive) counterpart to the round-144 5.0
3504    /// ACPL_2 real-α-β path
3505    /// ([`Self::encode_frame_pcm_5_0_acpl2_real_alpha_beta`]) and the
3506    /// real-α-β upgrade of the round-107 7.0 ACPL_2 zero-delta path
3507    /// ([`Self::encode_frame_pcm_7_0_acpl2`]).
3508    ///
3509    /// `frames` is in `[L, R, C, Ls, Rs, Lb, Rb]` order — the 7.0 (3/4/0)
3510    /// surface layout. The L/R pair feeds the first `two_channel_data()`
3511    /// carriers and drives the A-CPL Ls/Rs surround reconstruction via
3512    /// [`crate::acpl_synth::run_acpl_5x_pair_pcm`] at decode time; the
3513    /// Ls/Rs pair rides the second `two_channel_data()` *and* feeds the
3514    /// α + β extractors (D0 module models (L → Ls); D1 module models
3515    /// (R → Rs)). `acpl_config_1ch(FULL)` carries no `qmf_band` →
3516    /// `start_band = 0` so every parameter band participates. The centre
3517    /// `C` is the trailing Cfg0 `mono_data(0)`. The back pair `Lb, Rb`
3518    /// is accepted for layout completeness but not carried by the
3519    /// ASPX_ACPL_2 body (the decoder's 7_X ACPL_2 dispatch populates
3520    /// slots 0..4 — slots 5/6 stay silent), matching the round-107
3521    /// documented Table 202 channel mapping.
3522    ///
3523    /// The encoder forces the 7.0 channel_mode prefix (`0b1111000`, 7 b —
3524    /// Table 85 channel_mode 5) so the decoder's `walk_ac4_substream`
3525    /// dispatches `channels == 7` through
3526    /// `parse_7x_audio_data_outer(b_has_lfe = false)` with
3527    /// `7_X_codec_mode = AspxAcpl2`.
3528    ///
3529    /// `max_sfb` defaults to 40.
3530    pub fn encode_frame_pcm_7_0_acpl2_real_alpha_beta(&mut self, frames: &[&[f32]; 7]) -> Vec<u8> {
3531        self.encode_frame_pcm_7_0_acpl2_real_alpha_beta_with_max_sfb(frames, 40)
3532    }
3533
3534    /// `max_sfb`-parameterised form of
3535    /// [`Self::encode_frame_pcm_7_0_acpl2_real_alpha_beta`].
3536    pub fn encode_frame_pcm_7_0_acpl2_real_alpha_beta_with_max_sfb(
3537        &mut self,
3538        frames: &[&[f32]; 7],
3539        max_sfb: u32,
3540    ) -> Vec<u8> {
3541        let (_fps_milli, frame_len) =
3542            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
3543        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
3544        for (ch, f) in frames.iter().enumerate() {
3545            assert_eq!(
3546                f.len(),
3547                frame_len as usize,
3548                "encode_frame_pcm_7_0_acpl2_real_alpha_beta: channel {ch} input length must match frame_len = {frame_len}"
3549            );
3550        }
3551        let (n_msfb_bits, _, _) =
3552            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
3553        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
3554        let max_sfb = max_sfb.min(n_msfb_cap);
3555
3556        // Force 7.0 (3/4/0) channel_mode prefix '1111000', 7 b →
3557        // channel_mode 5 (Table 85).
3558        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
3559        self.channel_mode_value = 0b1111000;
3560        self.channel_mode_bits = 7;
3561
3562        // Forward MDCT analysis per channel — seven SCE states (L, R, C,
3563        // Ls, Rs, Lb, Rb). The first five feed the ASPX_ACPL_2 body;
3564        // Ls / Rs additionally feed the α + β extractors. The back pair
3565        // is analysed for state continuity but its spectra are not
3566        // carried by the ACPL_2 path.
3567        let n_channels = 7;
3568        while self.mdct_states_multi.len() < n_channels {
3569            self.mdct_states_multi
3570                .push(EncoderMdctState::new(frame_len));
3571        }
3572        for state in self.mdct_states_multi.iter_mut() {
3573            if state.n != frame_len {
3574                *state = EncoderMdctState::new(frame_len);
3575            }
3576        }
3577        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
3578        for (ch, f) in frames.iter().enumerate() {
3579            let c = self.mdct_states_multi[ch].analyse_frame(f);
3580            coeffs_per_channel.push(c);
3581        }
3582
3583        // ASPX config: matches the round-95 / 100 / 103 / 107 ASPX_ACPL
3584        // config exactly.
3585        let aspx_cfg = crate::aspx::AspxConfig {
3586            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
3587            start_freq: 0,
3588            stop_freq: 0,
3589            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
3590            interpolation: false,
3591            preflat: false,
3592            limiter: false,
3593            noise_sbg: 0, // num_noise_sbgroups = 1
3594            num_env_bits_fixfix: 0,
3595            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
3596        };
3597
3598        // ACPL: num_param_bands_id = 3 → 7 param bands; quant_mode Fine.
3599        let acpl_num_param_bands_id: u8 = 3;
3600        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
3601
3602        let pad_target_bytes: usize = match max_sfb {
3603            0..=20 => 4096,
3604            21..=40 => 12288,
3605            41..=50 => 24576,
3606            _ => 32767,
3607        };
3608
3609        let body = crate::encoder_acpl3::build_7_x_acpl2_body_from_pcm_spectra_real_alpha_beta(
3610            frame_len,
3611            max_sfb,
3612            None, // 7.0 — no LFE
3613            self.b_iframe_global,
3614            &coeffs_per_channel[0],
3615            &coeffs_per_channel[1],
3616            &coeffs_per_channel[3],
3617            &coeffs_per_channel[4],
3618            &coeffs_per_channel[2],
3619            None, // 7.0 — no LFE
3620            &aspx_cfg,
3621            acpl_num_param_bands_id,
3622            acpl_quant_mode,
3623            pad_target_bytes,
3624        );
3625
3626        // Wrap in v2 IMS TOC.
3627        let mut bw = BitWriter::new();
3628        self.write_toc(&mut bw);
3629        bw.align_to_byte();
3630        let mut out = bw.finish();
3631        out.extend(body);
3632        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
3633        self.channel_mode_value = saved_mode.0;
3634        self.channel_mode_bits = saved_mode.1;
3635        out
3636    }
3637
3638    /// Encode one IMS v2 frame containing a 7.1 (3/4/0.1) SIMPLE/ASPX_ACPL_2
3639    /// multichannel substream per ETSI TS 103 190-1 §4.2.6.14 Table 33 row
3640    /// `case ASPX_ACPL_2:` with `b_has_lfe = 1` and **real per-parameter-band
3641    /// α + β extraction** (round 202). The LFE counterpart of
3642    /// [`Self::encode_frame_pcm_7_0_acpl2_real_alpha_beta`] — it emits the
3643    /// identical 7_X ASPX_ACPL_2 real-α-β body plus a leading
3644    /// `mono_data(b_lfe = 1)` element between the I-frame config block and
3645    /// `companding_control(5)`, exactly where the decoder's
3646    /// `parse_7x_audio_data_outer(b_has_lfe = true)` reads
3647    /// `if (b_has_lfe) mono_data(1);`.
3648    ///
3649    /// `frames` is in `[L, R, C, Ls, Rs, Lb, Rb, LFE]` order. See
3650    /// [`Self::encode_frame_pcm_7_0_acpl2_real_alpha_beta`] for the channel
3651    /// routing contract; the LFE is the leading `mono_data(1)`.
3652    ///
3653    /// The encoder forces the 7.1 channel_mode prefix (`0b1111001`, 7 b —
3654    /// Table 88 channel_mode 6) so the decoder dispatches `channels == 8`
3655    /// through `parse_7x_audio_data_outer(b_has_lfe = true)` with
3656    /// `7_X_codec_mode = AspxAcpl2`.
3657    ///
3658    /// `max_sfb` defaults to 40; `max_sfb_lfe` defaults to 7 (the LFE-spec
3659    /// cap at `tl = 1920`, `n_msfbl_bits = 3`).
3660    pub fn encode_frame_pcm_7_1_acpl2_real_alpha_beta(&mut self, frames: &[&[f32]; 8]) -> Vec<u8> {
3661        self.encode_frame_pcm_7_1_acpl2_real_alpha_beta_with_max_sfb(frames, 40, 7)
3662    }
3663
3664    /// `max_sfb` / `max_sfb_lfe`-parameterised form of
3665    /// [`Self::encode_frame_pcm_7_1_acpl2_real_alpha_beta`].
3666    pub fn encode_frame_pcm_7_1_acpl2_real_alpha_beta_with_max_sfb(
3667        &mut self,
3668        frames: &[&[f32]; 8],
3669        max_sfb: u32,
3670        max_sfb_lfe: u32,
3671    ) -> Vec<u8> {
3672        let (_fps_milli, frame_len) =
3673            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
3674        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
3675        for (ch, f) in frames.iter().enumerate() {
3676            assert_eq!(
3677                f.len(),
3678                frame_len as usize,
3679                "encode_frame_pcm_7_1_acpl2_real_alpha_beta: channel {ch} input length must match frame_len = {frame_len}"
3680            );
3681        }
3682        let (n_msfb_bits, _, n_msfbl_bits) =
3683            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
3684        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
3685        let max_sfb = max_sfb.min(n_msfb_cap);
3686        assert!(
3687            n_msfbl_bits > 0,
3688            "encode_frame_pcm_7_1_acpl2_real_alpha_beta: tl = {frame_len} not permitted for LFE"
3689        );
3690        let n_msfbl_cap = (1u32 << n_msfbl_bits) - 1;
3691        let max_sfb_lfe = max_sfb_lfe.min(n_msfbl_cap);
3692
3693        // Force 7.1 (3/4/0.1) channel_mode prefix '1111001', 7 b →
3694        // channel_mode 6 (Table 88).
3695        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
3696        self.channel_mode_value = 0b1111001;
3697        self.channel_mode_bits = 7;
3698
3699        // Forward MDCT analysis per channel — eight SCE states (L, R, C,
3700        // Ls, Rs, Lb, Rb, LFE).
3701        let n_channels = 8;
3702        while self.mdct_states_multi.len() < n_channels {
3703            self.mdct_states_multi
3704                .push(EncoderMdctState::new(frame_len));
3705        }
3706        for state in self.mdct_states_multi.iter_mut() {
3707            if state.n != frame_len {
3708                *state = EncoderMdctState::new(frame_len);
3709            }
3710        }
3711        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
3712        for (ch, f) in frames.iter().enumerate() {
3713            let c = self.mdct_states_multi[ch].analyse_frame(f);
3714            coeffs_per_channel.push(c);
3715        }
3716
3717        // ASPX config: matches the round-95 / 100 / 103 / 107 / 114 ASPX_ACPL
3718        // config exactly.
3719        let aspx_cfg = crate::aspx::AspxConfig {
3720            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
3721            start_freq: 0,
3722            stop_freq: 0,
3723            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
3724            interpolation: false,
3725            preflat: false,
3726            limiter: false,
3727            noise_sbg: 0, // num_noise_sbgroups = 1
3728            num_env_bits_fixfix: 0,
3729            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
3730        };
3731
3732        // ACPL: num_param_bands_id = 3 → 7 param bands; quant_mode Fine.
3733        let acpl_num_param_bands_id: u8 = 3;
3734        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
3735
3736        let pad_target_bytes: usize = match max_sfb {
3737            0..=20 => 4096,
3738            21..=40 => 12288,
3739            41..=50 => 24576,
3740            _ => 32767,
3741        };
3742
3743        let body = crate::encoder_acpl3::build_7_x_acpl2_body_from_pcm_spectra_real_alpha_beta(
3744            frame_len,
3745            max_sfb,
3746            Some(max_sfb_lfe),
3747            self.b_iframe_global,
3748            &coeffs_per_channel[0],
3749            &coeffs_per_channel[1],
3750            &coeffs_per_channel[3],
3751            &coeffs_per_channel[4],
3752            &coeffs_per_channel[2],
3753            Some(&coeffs_per_channel[7]),
3754            &aspx_cfg,
3755            acpl_num_param_bands_id,
3756            acpl_quant_mode,
3757            pad_target_bytes,
3758        );
3759
3760        // Wrap in v2 IMS TOC.
3761        let mut bw = BitWriter::new();
3762        self.write_toc(&mut bw);
3763        bw.align_to_byte();
3764        let mut out = bw.finish();
3765        out.extend(body);
3766        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
3767        self.channel_mode_value = saved_mode.0;
3768        self.channel_mode_bits = saved_mode.1;
3769        out
3770    }
3771
3772    /// Encode one IMS v2 frame containing a 7.0 (3/4/0) SIMPLE/ASPX_ACPL_1
3773    /// multichannel substream per ETSI TS 103 190-1 §4.2.6.14 Table 33 row
3774    /// `case ASPX_ACPL_1:` (round 118). The 7_X (immersive) counterpart to
3775    /// the round-103 5_X ASPX_ACPL_1 encoder and the encoder side of the
3776    /// decoder's round-27 `parse_7x_audio_data_outer` ASPX_ACPL_1 branch.
3777    ///
3778    /// ASPX_ACPL_1 differs from the round-107 7.0 ASPX_ACPL_2 path in three
3779    /// structural places (the same three that separate the 5_X ACPL_1 path
3780    /// from the 5_X ACPL_2 path): `7_X_codec_mode = 2` (vs 3),
3781    /// `acpl_config_1ch` is PARTIAL (vs FULL — carries the 3-bit
3782    /// `acpl_qmf_band_minus1`), and the body carries an explicit joint-MDCT
3783    /// residual layer (`max_sfb_master + 2× chparam_info + 2× sf_data(ASF)`)
3784    /// transmitting the Ls/Rs surround pair (sSMP,3 / sSMP,4 per Table 181)
3785    /// rather than reconstructing it purely from the L/R carriers.
3786    ///
3787    /// `frames` is in `[L, R, C, Ls, Rs, Lb, Rb]` order — the 7.0 (3/4/0)
3788    /// surface layout. The L/R pair feeds the first `two_channel_data()`
3789    /// carriers; the Ls/Rs pair rides the second `two_channel_data()` *and*
3790    /// the joint-MDCT residual layer; the centre `C` is the trailing Cfg0
3791    /// `mono_data(0)`. The back pair `Lb, Rb` is accepted for layout
3792    /// completeness but not carried by the ASPX_ACPL_1 body (the decoder's
3793    /// 7_X ACPL_1 dispatch populates slots 0..4 only — slots 5/6 stay
3794    /// silent), matching the round-107 documented Table 202 channel mapping.
3795    ///
3796    /// The encoder forces the 7.0 (3/4/0) channel_mode prefix (`0b1111000`,
3797    /// 7 b — Table 85 channel_mode 5) so the decoder's `walk_ac4_substream`
3798    /// dispatches `channels == 7` through
3799    /// `parse_7x_audio_data_outer(b_has_lfe = false)` with
3800    /// `7_X_codec_mode = AspxAcpl1`. The ASPX/A-CPL parameter bits use the
3801    /// round-95 minimum-bit-cost zero-delta Huffman scaffold.
3802    ///
3803    /// `max_sfb` defaults to 40; `max_sfb_master` (the residual band bound)
3804    /// defaults to 20.
3805    pub fn encode_frame_pcm_7_0_acpl1(&mut self, frames: &[&[f32]; 7]) -> Vec<u8> {
3806        self.encode_frame_pcm_7_0_acpl1_with_max_sfb(frames, 40, 20)
3807    }
3808
3809    /// `max_sfb` / `max_sfb_master`-parameterised form of
3810    /// [`Self::encode_frame_pcm_7_0_acpl1`].
3811    pub fn encode_frame_pcm_7_0_acpl1_with_max_sfb(
3812        &mut self,
3813        frames: &[&[f32]; 7],
3814        max_sfb: u32,
3815        max_sfb_master: u32,
3816    ) -> Vec<u8> {
3817        let (_fps_milli, frame_len) =
3818            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
3819        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
3820        for (ch, f) in frames.iter().enumerate() {
3821            assert_eq!(
3822                f.len(),
3823                frame_len as usize,
3824                "encode_frame_pcm_7_0_acpl1: channel {ch} input length must match frame_len = {frame_len}"
3825            );
3826        }
3827        let (n_msfb_bits, _, _) =
3828            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
3829        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
3830        let max_sfb = max_sfb.min(n_msfb_cap);
3831
3832        // Force 7.0 (3/4/0) channel_mode prefix '1111000', 7 b →
3833        // channel_mode 5 (Table 85). The decoder routes channels == 7
3834        // through parse_7x_audio_data_outer(b_has_lfe = false).
3835        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
3836        self.channel_mode_value = 0b1111000;
3837        self.channel_mode_bits = 7;
3838
3839        // Forward MDCT analysis per channel — seven SCE states (L, R, C,
3840        // Ls, Rs, Lb, Rb). Only the first five feed the ASPX_ACPL_1 body;
3841        // the back pair is analysed for state continuity but its spectra
3842        // are not carried by the ACPL_1 path.
3843        let n_channels = 7;
3844        while self.mdct_states_multi.len() < n_channels {
3845            self.mdct_states_multi
3846                .push(EncoderMdctState::new(frame_len));
3847        }
3848        for state in self.mdct_states_multi.iter_mut() {
3849            if state.n != frame_len {
3850                *state = EncoderMdctState::new(frame_len);
3851            }
3852        }
3853        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
3854        for (ch, f) in frames.iter().enumerate() {
3855            let c = self.mdct_states_multi[ch].analyse_frame(f);
3856            coeffs_per_channel.push(c);
3857        }
3858
3859        // ASPX config: matches the round-95 / 100 / 103 / 107 ASPX_ACPL
3860        // config exactly (small low-res scale → small SBG counts).
3861        let aspx_cfg = crate::aspx::AspxConfig {
3862            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
3863            start_freq: 0,
3864            stop_freq: 0,
3865            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
3866            interpolation: false,
3867            preflat: false,
3868            limiter: false,
3869            noise_sbg: 0, // num_noise_sbgroups = 1
3870            num_env_bits_fixfix: 0,
3871            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
3872        };
3873
3874        // ACPL: num_param_bands_id = 3 → 7 param bands; quant_mode Fine;
3875        // acpl_qmf_band_minus1 = 0 → qmf_band = 1 (PARTIAL mode).
3876        let acpl_num_param_bands_id: u8 = 3;
3877        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
3878        let acpl_qmf_band_minus1: u8 = 0;
3879
3880        let pad_target_bytes: usize = match max_sfb {
3881            0..=20 => 4096,
3882            21..=40 => 12288,
3883            41..=50 => 24576,
3884            _ => 32767,
3885        };
3886
3887        let body = crate::encoder_acpl3::build_7_x_acpl1_body_from_pcm_spectra(
3888            frame_len,
3889            max_sfb,
3890            max_sfb_master,
3891            None, // 7.0 — no LFE
3892            self.b_iframe_global,
3893            &coeffs_per_channel[0],
3894            &coeffs_per_channel[1],
3895            &coeffs_per_channel[3],
3896            &coeffs_per_channel[4],
3897            &coeffs_per_channel[2],
3898            None, // 7.0 — no LFE
3899            &aspx_cfg,
3900            acpl_num_param_bands_id,
3901            acpl_quant_mode,
3902            acpl_qmf_band_minus1,
3903            pad_target_bytes,
3904        );
3905
3906        // Wrap in v2 IMS TOC.
3907        let mut bw = BitWriter::new();
3908        self.write_toc(&mut bw);
3909        bw.align_to_byte();
3910        let mut out = bw.finish();
3911        out.extend(body);
3912        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
3913        self.channel_mode_value = saved_mode.0;
3914        self.channel_mode_bits = saved_mode.1;
3915        out
3916    }
3917
3918    /// Encode one IMS v2 frame containing a 7.0 (3/4/0) SIMPLE/ASPX_ACPL_1
3919    /// multichannel substream with **real per-parameter-band α + β
3920    /// extraction** per ETSI TS 103 190-1 §5.7.7.5 Pseudocode 116 +
3921    /// §5.7.7.6.1 Pseudocode 117 (round 135).
3922    ///
3923    /// The 7_X immersive counterpart of
3924    /// [`Self::encode_frame_pcm_5_0_acpl1_real_alpha_beta`] and the real-
3925    /// α+β upgrade of [`Self::encode_frame_pcm_7_0_acpl1`] (which emitted
3926    /// both `acpl_data_1ch()` sets at the round-118 zero-delta scaffold).
3927    /// The two trailing `acpl_data_1ch()` parameter sets now carry the
3928    /// analytic α (from the L/Ls and R/Rs MDCT-energy correlation) and the
3929    /// β magnitude that closes the surround/carrier energy balance after α
3930    /// removes the level-only component:
3931    ///
3932    /// ```text
3933    ///   E[Ls²] = 0.5 · E[L²] · ( (1 − α)² + β² )
3934    /// ```
3935    ///
3936    /// `frames` is in `[L, R, C, Ls, Rs, Lb, Rb]` order — the 7.0 (3/4/0)
3937    /// surface layout, identical to [`Self::encode_frame_pcm_7_0_acpl1`].
3938    /// β / β3 / γ for non-ACPL_1 paths stay at the scaffold.
3939    pub fn encode_frame_pcm_7_0_acpl1_real_alpha_beta(&mut self, frames: &[&[f32]; 7]) -> Vec<u8> {
3940        self.encode_frame_pcm_7_0_acpl1_real_alpha_beta_with_max_sfb(frames, 40, 20)
3941    }
3942
3943    /// `max_sfb` / `max_sfb_master`-parameterised form of
3944    /// [`Self::encode_frame_pcm_7_0_acpl1_real_alpha_beta`].
3945    pub fn encode_frame_pcm_7_0_acpl1_real_alpha_beta_with_max_sfb(
3946        &mut self,
3947        frames: &[&[f32]; 7],
3948        max_sfb: u32,
3949        max_sfb_master: u32,
3950    ) -> Vec<u8> {
3951        let (_fps_milli, frame_len) =
3952            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
3953        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
3954        for (ch, f) in frames.iter().enumerate() {
3955            assert_eq!(
3956                f.len(),
3957                frame_len as usize,
3958                "encode_frame_pcm_7_0_acpl1_real_alpha_beta: channel {ch} input length must match frame_len = {frame_len}"
3959            );
3960        }
3961        let (n_msfb_bits, _, _) =
3962            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
3963        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
3964        let max_sfb = max_sfb.min(n_msfb_cap);
3965
3966        // Force 7.0 (3/4/0) channel_mode prefix '1111000', 7 b →
3967        // channel_mode 5 (Table 85). The decoder routes channels == 7
3968        // through parse_7x_audio_data_outer(b_has_lfe = false).
3969        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
3970        self.channel_mode_value = 0b1111000;
3971        self.channel_mode_bits = 7;
3972
3973        let n_channels = 7;
3974        while self.mdct_states_multi.len() < n_channels {
3975            self.mdct_states_multi
3976                .push(EncoderMdctState::new(frame_len));
3977        }
3978        for state in self.mdct_states_multi.iter_mut() {
3979            if state.n != frame_len {
3980                *state = EncoderMdctState::new(frame_len);
3981            }
3982        }
3983        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
3984        for (ch, f) in frames.iter().enumerate() {
3985            let c = self.mdct_states_multi[ch].analyse_frame(f);
3986            coeffs_per_channel.push(c);
3987        }
3988
3989        let aspx_cfg = crate::aspx::AspxConfig {
3990            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
3991            start_freq: 0,
3992            stop_freq: 0,
3993            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
3994            interpolation: false,
3995            preflat: false,
3996            limiter: false,
3997            noise_sbg: 0,
3998            num_env_bits_fixfix: 0,
3999            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
4000        };
4001
4002        let acpl_num_param_bands_id: u8 = 3;
4003        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
4004        let acpl_qmf_band_minus1: u8 = 0;
4005
4006        let pad_target_bytes: usize = match max_sfb {
4007            0..=20 => 4096,
4008            21..=40 => 12288,
4009            41..=50 => 24576,
4010            _ => 32767,
4011        };
4012
4013        let body = crate::encoder_acpl3::build_7_x_acpl1_body_from_pcm_spectra_real_alpha_beta(
4014            frame_len,
4015            max_sfb,
4016            max_sfb_master,
4017            None, // 7.0 — no LFE
4018            self.b_iframe_global,
4019            &coeffs_per_channel[0],
4020            &coeffs_per_channel[1],
4021            &coeffs_per_channel[3],
4022            &coeffs_per_channel[4],
4023            &coeffs_per_channel[2],
4024            None, // 7.0 — no LFE
4025            &aspx_cfg,
4026            acpl_num_param_bands_id,
4027            acpl_quant_mode,
4028            acpl_qmf_band_minus1,
4029            pad_target_bytes,
4030        );
4031
4032        let mut bw = BitWriter::new();
4033        self.write_toc(&mut bw);
4034        bw.align_to_byte();
4035        let mut out = bw.finish();
4036        out.extend(body);
4037        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
4038        self.channel_mode_value = saved_mode.0;
4039        self.channel_mode_bits = saved_mode.1;
4040        out
4041    }
4042
4043    /// Encode one IMS v2 frame containing a 7.1 (3/4/0.1) SIMPLE/ASPX_ACPL_1
4044    /// multichannel substream per ETSI TS 103 190-1 §4.2.6.14 Table 33 row
4045    /// `case ASPX_ACPL_1:` with `b_has_lfe = 1` (round 118). The LFE
4046    /// counterpart of [`Self::encode_frame_pcm_7_0_acpl1`] — it emits the
4047    /// identical 7_X ASPX_ACPL_1 body plus a leading `mono_data(b_lfe = 1)`
4048    /// element (Table 21 + `sf_info_lfe()` Table 35) between the I-frame
4049    /// config block and `companding_control(5)`, exactly where the decoder's
4050    /// `parse_7x_audio_data_outer(b_has_lfe = true)` reads
4051    /// `if (b_has_lfe) mono_data(1);`.
4052    ///
4053    /// `frames` is in `[L, R, C, Ls, Rs, Lb, Rb, LFE]` order — the 7.1
4054    /// (3/4/0.1) surface layout. The LFE is the leading `mono_data(1)`; the
4055    /// rest of the body matches the 7.0 ACPL_1 form. The encoder forces the
4056    /// 7.1 channel_mode prefix (`0b1111001`, 7 b — Table 88 channel_mode 6)
4057    /// so the decoder dispatches `channels == 8` through
4058    /// `parse_7x_audio_data_outer(b_has_lfe = true)` with
4059    /// `7_X_codec_mode = AspxAcpl1`; the LFE spectrum IMDCT's into slot 7
4060    /// via the round-80 LFE PCM render.
4061    ///
4062    /// `max_sfb` defaults to 40; `max_sfb_master` defaults to 20;
4063    /// `max_sfb_lfe` defaults to 7 (the LFE-spec cap at `tl = 1920`,
4064    /// `n_msfbl_bits = 3`).
4065    pub fn encode_frame_pcm_7_1_acpl1(&mut self, frames: &[&[f32]; 8]) -> Vec<u8> {
4066        self.encode_frame_pcm_7_1_acpl1_with_max_sfb(frames, 40, 20, 7)
4067    }
4068
4069    /// `max_sfb`-parameterised form of [`Self::encode_frame_pcm_7_1_acpl1`].
4070    /// `max_sfb` governs the five front/surround carrier SCEs and the centre
4071    /// mono; `max_sfb_master` governs the joint-MDCT surround residual
4072    /// layer; `max_sfb_lfe` governs the LFE `mono_data(1)` (clamped to the
4073    /// `n_msfbl_bits` cap).
4074    pub fn encode_frame_pcm_7_1_acpl1_with_max_sfb(
4075        &mut self,
4076        frames: &[&[f32]; 8],
4077        max_sfb: u32,
4078        max_sfb_master: u32,
4079        max_sfb_lfe: u32,
4080    ) -> Vec<u8> {
4081        let (_fps_milli, frame_len) =
4082            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
4083        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
4084        for (ch, f) in frames.iter().enumerate() {
4085            assert_eq!(
4086                f.len(),
4087                frame_len as usize,
4088                "encode_frame_pcm_7_1_acpl1: channel {ch} input length must match frame_len = {frame_len}"
4089            );
4090        }
4091        let (n_msfb_bits, _, n_msfbl_bits) =
4092            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
4093        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
4094        let max_sfb = max_sfb.min(n_msfb_cap);
4095        assert!(
4096            n_msfbl_bits > 0,
4097            "encode_frame_pcm_7_1_acpl1: tl = {frame_len} not permitted for LFE"
4098        );
4099        let n_msfbl_cap = (1u32 << n_msfbl_bits) - 1;
4100        let max_sfb_lfe = max_sfb_lfe.min(n_msfbl_cap);
4101
4102        // Force 7.1 (3/4/0.1) channel_mode prefix '1111001', 7 b →
4103        // channel_mode 6 (Table 88). The decoder routes channels == 8
4104        // through parse_7x_audio_data_outer(b_has_lfe = true).
4105        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
4106        self.channel_mode_value = 0b1111001;
4107        self.channel_mode_bits = 7;
4108
4109        // Forward MDCT analysis per channel — eight SCE states (L, R, C,
4110        // Ls, Rs, Lb, Rb, LFE). The first five + LFE feed the ASPX_ACPL_1
4111        // body; the back pair is analysed for state continuity but its
4112        // spectra are not carried by the ACPL_1 path.
4113        let n_channels = 8;
4114        while self.mdct_states_multi.len() < n_channels {
4115            self.mdct_states_multi
4116                .push(EncoderMdctState::new(frame_len));
4117        }
4118        for state in self.mdct_states_multi.iter_mut() {
4119            if state.n != frame_len {
4120                *state = EncoderMdctState::new(frame_len);
4121            }
4122        }
4123        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
4124        for (ch, f) in frames.iter().enumerate() {
4125            let c = self.mdct_states_multi[ch].analyse_frame(f);
4126            coeffs_per_channel.push(c);
4127        }
4128
4129        // ASPX config: matches the round-95 / 100 / 103 / 107 ASPX_ACPL
4130        // config exactly (small low-res scale → small SBG counts).
4131        let aspx_cfg = crate::aspx::AspxConfig {
4132            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
4133            start_freq: 0,
4134            stop_freq: 0,
4135            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
4136            interpolation: false,
4137            preflat: false,
4138            limiter: false,
4139            noise_sbg: 0, // num_noise_sbgroups = 1
4140            num_env_bits_fixfix: 0,
4141            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
4142        };
4143
4144        // ACPL: num_param_bands_id = 3 → 7 param bands; quant_mode Fine;
4145        // acpl_qmf_band_minus1 = 0 → qmf_band = 1 (PARTIAL mode).
4146        let acpl_num_param_bands_id: u8 = 3;
4147        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
4148        let acpl_qmf_band_minus1: u8 = 0;
4149
4150        let pad_target_bytes: usize = match max_sfb {
4151            0..=20 => 4096,
4152            21..=40 => 12288,
4153            41..=50 => 24576,
4154            _ => 32767,
4155        };
4156
4157        let body = crate::encoder_acpl3::build_7_x_acpl1_body_from_pcm_spectra(
4158            frame_len,
4159            max_sfb,
4160            max_sfb_master,
4161            Some(max_sfb_lfe),
4162            self.b_iframe_global,
4163            &coeffs_per_channel[0],
4164            &coeffs_per_channel[1],
4165            &coeffs_per_channel[3],
4166            &coeffs_per_channel[4],
4167            &coeffs_per_channel[2],
4168            Some(&coeffs_per_channel[7]),
4169            &aspx_cfg,
4170            acpl_num_param_bands_id,
4171            acpl_quant_mode,
4172            acpl_qmf_band_minus1,
4173            pad_target_bytes,
4174        );
4175
4176        // Wrap in v2 IMS TOC.
4177        let mut bw = BitWriter::new();
4178        self.write_toc(&mut bw);
4179        bw.align_to_byte();
4180        let mut out = bw.finish();
4181        out.extend(body);
4182        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
4183        self.channel_mode_value = saved_mode.0;
4184        self.channel_mode_bits = saved_mode.1;
4185        out
4186    }
4187
4188    /// Encode one IMS v2 frame containing a 7.1 (3/4/0.1) SIMPLE/ASPX_ACPL_1
4189    /// multichannel substream per ETSI TS 103 190-1 §4.2.6.14 Table 33 row
4190    /// `case ASPX_ACPL_1:` with `b_has_lfe = 1`, with **real per-parameter-band
4191    /// α + β extraction** carried by the two trailing `acpl_data_1ch()`
4192    /// parameter sets (round 139 — the LFE counterpart of the round-135
4193    /// 7.0 immersive real-α+β path,
4194    /// [`Self::encode_frame_pcm_7_0_acpl1_real_alpha_beta`]).
4195    ///
4196    /// The round-118 7.1 ASPX_ACPL_1 encoder emitted both `acpl_data_1ch()`
4197    /// parameter sets at the zero-delta scaffold; this entry point upgrades
4198    /// them to carry the analytic α (from the L/Ls and R/Rs MDCT-energy
4199    /// correlation, §5.7.7.5 Pseudocode 116) plus the β magnitude that
4200    /// closes the surround/carrier energy balance after α removes the
4201    /// level-only component (§5.7.7.6.1 Pseudocode 117):
4202    ///
4203    /// ```text
4204    ///   E[Ls²] = 0.5 · E[L²] · ( (1 − α)² + β² )
4205    ///   ⇒  β = √max(0, 2·E[Ls²]/E[L²] − (1 − α)²)
4206    /// ```
4207    ///
4208    /// `frames` is in `[L, R, C, Ls, Rs, Lb, Rb, LFE]` order — the 7.1
4209    /// (3/4/0.1) surface layout, identical to
4210    /// [`Self::encode_frame_pcm_7_1_acpl1`]. The leading `mono_data(b_lfe = 1)`
4211    /// element (Table 21 + `sf_info_lfe()` Table 35) is emitted between the
4212    /// I-frame config block and `companding_control(5)`. The on-wire body
4213    /// structure is otherwise identical — decoder resolves
4214    /// `SevenXCodecMode::AspxAcpl1` with `b_has_lfe = true`, both
4215    /// `acpl_data_1ch_pair[0/1]` populated (now carrying real α + β),
4216    /// joint-MDCT residual layer walked, LFE IMDCT'd into slot 7.
4217    pub fn encode_frame_pcm_7_1_acpl1_real_alpha_beta(&mut self, frames: &[&[f32]; 8]) -> Vec<u8> {
4218        self.encode_frame_pcm_7_1_acpl1_real_alpha_beta_with_max_sfb(frames, 40, 20, 7)
4219    }
4220
4221    /// `max_sfb`-parameterised form of
4222    /// [`Self::encode_frame_pcm_7_1_acpl1_real_alpha_beta`]. `max_sfb`
4223    /// governs the five front/surround carrier SCEs and the centre mono;
4224    /// `max_sfb_master` governs the joint-MDCT surround residual layer;
4225    /// `max_sfb_lfe` governs the LFE `mono_data(1)` (clamped to the
4226    /// `n_msfbl_bits` cap).
4227    pub fn encode_frame_pcm_7_1_acpl1_real_alpha_beta_with_max_sfb(
4228        &mut self,
4229        frames: &[&[f32]; 8],
4230        max_sfb: u32,
4231        max_sfb_master: u32,
4232        max_sfb_lfe: u32,
4233    ) -> Vec<u8> {
4234        let (_fps_milli, frame_len) =
4235            crate::toc::frame_rate_entry(self.frame_rate_index as u32, self.fs_index as u32);
4236        let frame_len = if frame_len == 0 { 1920 } else { frame_len };
4237        for (ch, f) in frames.iter().enumerate() {
4238            assert_eq!(
4239                f.len(),
4240                frame_len as usize,
4241                "encode_frame_pcm_7_1_acpl1_real_alpha_beta: channel {ch} input length must match frame_len = {frame_len}"
4242            );
4243        }
4244        let (n_msfb_bits, _, n_msfbl_bits) =
4245            crate::tables::n_msfb_bits_48(frame_len).expect("encoder: bad tl");
4246        let n_msfb_cap = (1u32 << n_msfb_bits) - 1;
4247        let max_sfb = max_sfb.min(n_msfb_cap);
4248        assert!(
4249            n_msfbl_bits > 0,
4250            "encode_frame_pcm_7_1_acpl1_real_alpha_beta: tl = {frame_len} not permitted for LFE"
4251        );
4252        let n_msfbl_cap = (1u32 << n_msfbl_bits) - 1;
4253        let max_sfb_lfe = max_sfb_lfe.min(n_msfbl_cap);
4254
4255        // Force 7.1 (3/4/0.1) channel_mode prefix '1111001', 7 b →
4256        // channel_mode 6 (Table 88). The decoder routes channels == 8
4257        // through parse_7x_audio_data_outer(b_has_lfe = true).
4258        let saved_mode = (self.channel_mode_value, self.channel_mode_bits);
4259        self.channel_mode_value = 0b1111001;
4260        self.channel_mode_bits = 7;
4261
4262        let n_channels = 8;
4263        while self.mdct_states_multi.len() < n_channels {
4264            self.mdct_states_multi
4265                .push(EncoderMdctState::new(frame_len));
4266        }
4267        for state in self.mdct_states_multi.iter_mut() {
4268            if state.n != frame_len {
4269                *state = EncoderMdctState::new(frame_len);
4270            }
4271        }
4272        let mut coeffs_per_channel: Vec<Vec<f32>> = Vec::with_capacity(n_channels);
4273        for (ch, f) in frames.iter().enumerate() {
4274            let c = self.mdct_states_multi[ch].analyse_frame(f);
4275            coeffs_per_channel.push(c);
4276        }
4277
4278        let aspx_cfg = crate::aspx::AspxConfig {
4279            quant_mode_env: crate::aspx::AspxQuantStep::Fine,
4280            start_freq: 0,
4281            stop_freq: 0,
4282            master_freq_scale: crate::aspx::AspxMasterFreqScale::LowRes,
4283            interpolation: false,
4284            preflat: false,
4285            limiter: false,
4286            noise_sbg: 0,
4287            num_env_bits_fixfix: 0,
4288            freq_res_mode: crate::aspx::AspxFreqResMode::DurationDependent,
4289        };
4290
4291        let acpl_num_param_bands_id: u8 = 3;
4292        let acpl_quant_mode = crate::acpl::AcplQuantMode::Fine;
4293        let acpl_qmf_band_minus1: u8 = 0;
4294
4295        let pad_target_bytes: usize = match max_sfb {
4296            0..=20 => 4096,
4297            21..=40 => 12288,
4298            41..=50 => 24576,
4299            _ => 32767,
4300        };
4301
4302        let body = crate::encoder_acpl3::build_7_x_acpl1_body_from_pcm_spectra_real_alpha_beta(
4303            frame_len,
4304            max_sfb,
4305            max_sfb_master,
4306            Some(max_sfb_lfe),
4307            self.b_iframe_global,
4308            &coeffs_per_channel[0],
4309            &coeffs_per_channel[1],
4310            &coeffs_per_channel[3],
4311            &coeffs_per_channel[4],
4312            &coeffs_per_channel[2],
4313            Some(&coeffs_per_channel[7]),
4314            &aspx_cfg,
4315            acpl_num_param_bands_id,
4316            acpl_quant_mode,
4317            acpl_qmf_band_minus1,
4318            pad_target_bytes,
4319        );
4320
4321        let mut bw = BitWriter::new();
4322        self.write_toc(&mut bw);
4323        bw.align_to_byte();
4324        let mut out = bw.finish();
4325        out.extend(body);
4326        self.sequence_counter = (self.sequence_counter.wrapping_add(1)) & 0x3FF;
4327        self.channel_mode_value = saved_mode.0;
4328        self.channel_mode_bits = saved_mode.1;
4329        out
4330    }
4331
4332    /// Encode one IMS v2 frame containing a mono SIMPLE/ASF substream
4333    /// whose injected tone falls on the spectral pair nearest the
4334    /// requested frequency. With `tl = 1920` at 48 kHz the bin spacing
4335    /// is 12.5 Hz; the chosen pair carries a single non-zero quantised
4336    /// value at the lower bin of that pair.
4337    ///
4338    /// Returns the encoded frame bytes plus the actual nominal centre
4339    /// frequency the encoder targeted (lower-bin × bin_spacing).
4340    pub fn encode_frame_mono_tone_at_hz(&mut self, target_hz: f32) -> (Vec<u8>, f32) {
4341        let bin_spacing = 48_000.0 / (2.0 * 1_920.0); // 12.5 Hz
4342        let target_bin = (target_hz / bin_spacing).round().max(0.0) as u32;
4343        let pair_idx = target_bin / 2;
4344        let actual_hz = (pair_idx * 2) as f32 * bin_spacing;
4345        // cb_idx 49 → (q0=+1, q1=0): tone in lower bin of the pair.
4346        let frame = self.encode_frame_mono_tone(49, pair_idx);
4347        (frame, actual_hz)
4348    }
4349}
4350
4351#[cfg(test)]
4352mod tests {
4353    use super::*;
4354
4355    #[test]
4356    fn encoder_emits_nonempty_frame() {
4357        let mut enc = Ac4ImsEncoder::new();
4358        let frame = enc.encode_frame(0);
4359        assert!(!frame.is_empty(), "encoder must produce at least the TOC");
4360        // sequence_counter rolled from 0 → 1.
4361        assert_eq!(enc.sequence_counter, 1);
4362    }
4363
4364    #[test]
4365    fn encoder_sequence_counter_wraps_at_1024() {
4366        let mut enc = Ac4ImsEncoder::new();
4367        enc.sequence_counter = 1023;
4368        let _ = enc.encode_frame(0);
4369        // 1023 + 1 = 1024 → wraps to 0 (10-bit field).
4370        assert_eq!(enc.sequence_counter, 0);
4371    }
4372
4373    #[test]
4374    fn v0_encoder_round_trips_through_parse_ac4_toc() {
4375        // encode_frame_v0 emits a TS 103 190-1 TOC the existing
4376        // `parse_ac4_toc` walker accepts without erroring. Round-trip
4377        // (encode → parse) must return the same metadata we encoded:
4378        // mono / 48 kHz / 24 fps / iframe_global / 1920 samples per
4379        // frame.
4380        let mut enc = Ac4ImsEncoder::new(); // mono default
4381        let frame = enc.encode_frame_v0(64);
4382        let info = crate::toc::parse_ac4_toc(&frame).expect("v0 TOC must parse");
4383        assert_eq!(info.fs_index, 1);
4384        assert_eq!(info.frame_rate_index, 1);
4385        assert_eq!(info.frame_length, 1_920);
4386        assert!(info.b_iframe_global);
4387        assert_eq!(info.n_presentations, 1);
4388        assert_eq!(info.n_substreams, 1);
4389        // mono channel mode prefix '0' → 1 channel.
4390        assert_eq!(info.channels, 1);
4391    }
4392
4393    #[test]
4394    fn v0_encoder_round_trips_stereo() {
4395        let mut enc = Ac4ImsEncoder::new().with_v0().with_stereo();
4396        let frame = enc.encode_frame(64);
4397        let info = crate::toc::parse_ac4_toc(&frame).expect("v0 stereo TOC must parse");
4398        assert_eq!(info.channels, 2);
4399        assert!(info.b_iframe_global);
4400    }
4401
4402    #[test]
4403    fn v0_encoder_round_trips_5_1() {
4404        let mut enc = Ac4ImsEncoder::new().with_v0().with_5_1();
4405        let frame = enc.encode_frame(128);
4406        let info = crate::toc::parse_ac4_toc(&frame).expect("v0 5.1 TOC must parse");
4407        // channel_mode prefix '1110' → 6 channels (5.1) per Table 85.
4408        assert_eq!(info.channels, 6);
4409    }
4410
4411    #[test]
4412    fn v0_encoder_decoder_roundtrip_emits_silent_frame() {
4413        // Full encode → Ac4Decoder roundtrip on the v0 path. The
4414        // decoder accepts the Auditor frame (TOC + zero body) and
4415        // emits a structurally-valid silent AudioFrame at the
4416        // declared 1920 samples / 48 kHz / mono shape.
4417        use crate::decoder::Ac4Decoder;
4418        use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
4419        let mut enc = Ac4ImsEncoder::new(); // mono default
4420        let frame_bytes = enc.encode_frame_v0(64);
4421        let params = CodecParameters::audio(CodecId::new("ac4"));
4422        let mut dec = Ac4Decoder::new(&params);
4423        let pkt = Packet::new(0, TimeBase::new(1, 48_000), frame_bytes);
4424        dec.send_packet(&pkt).expect("send_packet");
4425        let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
4426            panic!("expected audio frame");
4427        };
4428        assert_eq!(af.samples, 1_920);
4429        // mono S16 layout: 1920 samples × 1 ch × 2 bytes.
4430        assert_eq!(af.data.len(), 1);
4431        assert_eq!(af.data[0].len(), 1_920 * 2);
4432        // All bytes should be zero (silent placeholder).
4433        assert!(af.data[0].iter().all(|&b| b == 0));
4434    }
4435
4436    #[test]
4437    fn v2_encoder_emits_first_two_bits_as_bitstream_version_2() {
4438        // Auditor-mode contract: the first two bits of the produced
4439        // frame are `bitstream_version = 0b10` (i.e. value 2). This
4440        // is the spec invariant from Table 74 — every TS 103 190-2
4441        // IMS bitstream MUST start with these bits.
4442        let mut enc = Ac4ImsEncoder::new(); // bitstream_version = 2
4443        let frame = enc.encode_frame(0);
4444        assert!(!frame.is_empty());
4445        let bv = (frame[0] >> 6) & 0b11;
4446        assert_eq!(bv, 0b10, "IMS frame must start with bitstream_version = 2");
4447    }
4448
4449    #[test]
4450    fn v0_encoder_emits_first_two_bits_as_bitstream_version_0() {
4451        let mut enc = Ac4ImsEncoder::new().with_v0();
4452        let frame = enc.encode_frame(0);
4453        assert!(!frame.is_empty());
4454        let bv = (frame[0] >> 6) & 0b11;
4455        assert_eq!(bv, 0b00, "v0 frame must start with bitstream_version = 0");
4456    }
4457
4458    #[test]
4459    fn v2_encoder_round_trips_through_parse_ac4_toc() {
4460        // Round-47 contract: the v2 TOC emitted by `encode_frame()`
4461        // round-trips through `parse_ac4_toc`. Mono / 48 kHz / 24 fps /
4462        // iframe_global / 1920 samples per frame should land on the
4463        // returned `Ac4FrameInfo` exactly as configured.
4464        let mut enc = Ac4ImsEncoder::new(); // v2 default, mono
4465        let frame = enc.encode_frame(64);
4466        let info = crate::toc::parse_ac4_toc(&frame).expect("v2 TOC must parse");
4467        assert_eq!(info.bitstream_version, 2);
4468        assert_eq!(info.fs_index, 1);
4469        assert_eq!(info.frame_rate_index, 1);
4470        assert_eq!(info.frame_length, 1_920);
4471        assert!(info.b_iframe_global);
4472        assert_eq!(info.n_presentations, 1);
4473        assert_eq!(info.n_substreams, 1);
4474    }
4475
4476    #[test]
4477    fn v2_encoder_round_trips_stereo() {
4478        let mut enc = Ac4ImsEncoder::new().with_stereo(); // v2, stereo
4479        let frame = enc.encode_frame(64);
4480        let info = crate::toc::parse_ac4_toc(&frame).expect("v2 stereo TOC must parse");
4481        assert_eq!(info.bitstream_version, 2);
4482        assert!(info.b_iframe_global);
4483    }
4484
4485    #[test]
4486    fn v2_encoder_round_trips_5_1() {
4487        let mut enc = Ac4ImsEncoder::new().with_5_1(); // v2, 5.1
4488        let frame = enc.encode_frame(128);
4489        let info = crate::toc::parse_ac4_toc(&frame).expect("v2 5.1 TOC must parse");
4490        assert_eq!(info.bitstream_version, 2);
4491    }
4492
4493    #[test]
4494    fn v2_encoder_mono_tone_roundtrip_emits_nonsilent_pcm() {
4495        // Round-47 IMS audio body: encode v2 frame containing a mono
4496        // SIMPLE/ASF substream with a single quantised spectral line at
4497        // (sfb=0, bin=0). Through the decoder's full Huffman → IMDCT →
4498        // KBD overlap-add chain this should produce real PCM with
4499        // non-trivial energy.
4500        use crate::decoder::Ac4Decoder;
4501        use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
4502        let mut enc = Ac4ImsEncoder::new(); // v2, mono
4503                                            // cb_idx 49 → (q0=+1, q1=0); pair_idx 0 → bin 0.
4504        let frame_bytes = enc.encode_frame_mono_tone(49, 0);
4505        let params = CodecParameters::audio(CodecId::new("ac4"));
4506        let mut dec = Ac4Decoder::new(&params);
4507        let pkt = Packet::new(0, TimeBase::new(1, 48_000), frame_bytes);
4508        dec.send_packet(&pkt).expect("send_packet");
4509        let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
4510            panic!("expected audio frame");
4511        };
4512        assert_eq!(af.samples, 1_920);
4513        assert_eq!(af.data.len(), 1);
4514        assert_eq!(af.data[0].len(), 1_920 * 2);
4515        // Decoded PCM must be non-silent.
4516        let samples_i16: Vec<i16> = af.data[0]
4517            .chunks_exact(2)
4518            .map(|c| i16::from_le_bytes([c[0], c[1]]))
4519            .collect();
4520        let nonzero_count = samples_i16.iter().filter(|&&s| s != 0).count();
4521        assert!(
4522            nonzero_count > 100,
4523            "expected non-silent PCM from IMS tone encoder, got {nonzero_count} non-zero samples"
4524        );
4525        let energy: i64 = samples_i16.iter().map(|&s| (s as i64) * (s as i64)).sum();
4526        assert!(energy > 0, "zero-energy tone output from IMS encoder");
4527        // Substream parse must have surfaced non-zero scaled spectra at
4528        // bin 0 (the tone we injected).
4529        let sub = dec.last_substream.as_ref().unwrap();
4530        let scaled = sub.tools.scaled_spec_primary.as_ref().unwrap();
4531        assert!(scaled[0].abs() > 0.0, "DC bin must carry the injected tone");
4532    }
4533
4534    #[test]
4535    fn v2_encoder_mono_tone_at_440hz_has_spectral_peak_near_target() {
4536        // Round-47 closed-form tone encoder targeting 440 Hz. With
4537        // tl = 1920 / fs = 48 kHz, bin_spacing = 12.5 Hz so the tone
4538        // pair lands at pair 17 (bin 34, ~425 Hz). The decoder's
4539        // scaled spectrum should carry a non-zero value at the
4540        // targeted bin.
4541        use crate::decoder::Ac4Decoder;
4542        use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
4543        let mut enc = Ac4ImsEncoder::new();
4544        let (frame_bytes, actual_hz) = enc.encode_frame_mono_tone_at_hz(440.0);
4545        // Encoder rounded 440 Hz → bin 35 → pair 17 → bin 34 (lower-of-pair).
4546        // Actual emitted frequency is 34 × 12.5 = 425.0 Hz.
4547        assert!(
4548            (actual_hz - 425.0).abs() < 1.0,
4549            "expected ~425 Hz target, got {actual_hz}"
4550        );
4551        let params = CodecParameters::audio(CodecId::new("ac4"));
4552        let mut dec = Ac4Decoder::new(&params);
4553        let pkt = Packet::new(0, TimeBase::new(1, 48_000), frame_bytes);
4554        dec.send_packet(&pkt).expect("send_packet");
4555        let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
4556            panic!("expected audio frame");
4557        };
4558        assert_eq!(af.samples, 1_920);
4559        // Spectral peak: scaled_spec[34] (lower bin of the targeted
4560        // pair) must be non-zero; the surrounding bins must NOT carry
4561        // the same peak (proves the tone is localised).
4562        let sub = dec.last_substream.as_ref().unwrap();
4563        let scaled = sub.tools.scaled_spec_primary.as_ref().unwrap();
4564        let target_bin = 34usize;
4565        assert!(
4566            scaled[target_bin].abs() > 0.0,
4567            "expected non-zero spectral coefficient at bin {target_bin}, got {}",
4568            scaled[target_bin]
4569        );
4570        // PCM should still be non-silent.
4571        let samples_i16: Vec<i16> = af.data[0]
4572            .chunks_exact(2)
4573            .map(|c| i16::from_le_bytes([c[0], c[1]]))
4574            .collect();
4575        let nonzero_count = samples_i16.iter().filter(|&&s| s != 0).count();
4576        assert!(
4577            nonzero_count > 100,
4578            "expected non-silent PCM at 440 Hz, got {nonzero_count} non-zero samples"
4579        );
4580    }
4581
4582    #[test]
4583    fn v2_encoder_decoder_roundtrip_emits_silent_frame() {
4584        // Full encode → Ac4Decoder roundtrip on the v2 path. The
4585        // decoder accepts the IMS frame (TOC + zero body) and emits a
4586        // structurally-valid silent AudioFrame at the declared 1920
4587        // samples / 48 kHz / mono shape.
4588        use crate::decoder::Ac4Decoder;
4589        use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
4590        let mut enc = Ac4ImsEncoder::new(); // v2 default, mono
4591        let frame_bytes = enc.encode_frame(64);
4592        let params = CodecParameters::audio(CodecId::new("ac4"));
4593        let mut dec = Ac4Decoder::new(&params);
4594        let pkt = Packet::new(0, TimeBase::new(1, 48_000), frame_bytes);
4595        dec.send_packet(&pkt).expect("send_packet");
4596        let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
4597            panic!("expected audio frame");
4598        };
4599        assert_eq!(af.samples, 1_920);
4600        // mono S16 layout: 1920 samples × 1 ch × 2 bytes.
4601        assert_eq!(af.data.len(), 1);
4602        assert_eq!(af.data[0].len(), 1_920 * 2);
4603        // All bytes should be zero (silent placeholder for the v2
4604        // audio body, which the encoder emits as raw zero bits).
4605        assert!(af.data[0].iter().all(|&b| b == 0));
4606    }
4607
4608    // ------------------------------------------------------------------
4609    // Round 48 — encode_frame_pcm: arbitrary float PCM input through the
4610    // full forward MDCT + scalefactor + ASF entropy chain.
4611    // ------------------------------------------------------------------
4612
4613    /// Helper: feed a sequence of PCM frames through the encoder, then
4614    /// the decoder, and return the decoded i16 PCM concatenated. The
4615    /// first decoded frame loses half a window to the encoder's zero
4616    /// history; callers that compare against the input should ignore it.
4617    fn encode_decode_frames(frames: &[Vec<f32>]) -> Vec<Vec<i16>> {
4618        use crate::decoder::Ac4Decoder;
4619        use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
4620        let params = CodecParameters::audio(CodecId::new("ac4"));
4621        let mut dec = Ac4Decoder::new(&params);
4622        let mut enc = Ac4ImsEncoder::new(); // v2, mono, 48 kHz, 24 fps
4623        let mut out: Vec<Vec<i16>> = Vec::with_capacity(frames.len());
4624        for (idx, f) in frames.iter().enumerate() {
4625            let bytes = enc.encode_frame_pcm(f);
4626            let _ = idx;
4627            let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
4628            dec.send_packet(&pkt).expect("send_packet");
4629            let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
4630                panic!("expected audio frame");
4631            };
4632            assert_eq!(af.samples, 1_920);
4633            assert_eq!(af.data.len(), 1);
4634            let pcm: Vec<i16> = af.data[0]
4635                .chunks_exact(2)
4636                .map(|c| i16::from_le_bytes([c[0], c[1]]))
4637                .collect();
4638            out.push(pcm);
4639        }
4640        out
4641    }
4642
4643    /// 1 kHz pure tone @ 48 kHz: encode → decode → assert spectral peak
4644    /// in the right neighbourhood. With tl = 1920, bin_spacing =
4645    /// 48_000 / (2 * 1920) = 12.5 Hz, so 1000 Hz lands at bin 80.
4646    #[test]
4647    fn encode_frame_pcm_1khz_tone_round_trips_with_spectral_peak() {
4648        // Generate 4 frames of a continuous 1 kHz sine wave so the MDCT
4649        // overlap-add reaches steady state.
4650        let n = 1920usize;
4651        let fs = 48_000.0_f32;
4652        let f = 1000.0_f32;
4653        let make_frame = |start: usize| -> Vec<f32> {
4654            (0..n)
4655                .map(|i| {
4656                    let t = (start + i) as f32 / fs;
4657                    0.3 * (2.0 * std::f32::consts::PI * f * t).sin()
4658                })
4659                .collect()
4660        };
4661        let frames: Vec<Vec<f32>> = (0..4).map(|i| make_frame(i * n)).collect();
4662        let decoded = encode_decode_frames(&frames);
4663        // Steady-state decoded frame: index 2.
4664        let pcm = &decoded[2];
4665        // Verify non-silent output.
4666        let nonzero = pcm.iter().filter(|&&s| s != 0).count();
4667        assert!(
4668            nonzero > 100,
4669            "expected non-silent PCM from 1 kHz tone, got {nonzero} non-zero samples"
4670        );
4671        // Energy must be substantial (input amplitude was 0.3 → expect
4672        // peak |i16| >= ~1000 at the centre of the steady-state frame).
4673        let peak = pcm.iter().map(|&s| s.abs()).max().unwrap_or(0);
4674        assert!(peak > 1000, "expected peak amplitude > 1000, got {peak}");
4675    }
4676
4677    /// Multi-tone audio: encode → decode → assert SNR > 10 dB on the
4678    /// steady-state frame. Uses a sum of three pure tones (250 Hz +
4679    /// 500 Hz + 1 kHz at amplitude 0.2 each) so the input is non-trivial
4680    /// (multi-line spectrum) but bandlimited well below the encoder's
4681    /// 7.5 kHz max_sfb=40 cutoff. This stands in for the spec's
4682    /// "white-noise SNR > 30 dB" target — round 48's HCB5-only quantiser
4683    /// caps |q| ≤ 4 (~12 dB SNR ceiling per band) and only codes
4684    /// 0..7.5 kHz, so true white noise is out of reach until round 49
4685    /// adds a wider codebook selector and a wider max_sfb.
4686    #[test]
4687    fn encode_frame_pcm_multitone_round_trips_with_positive_snr() {
4688        let n = 1920usize;
4689        let fs = 48_000.0_f32;
4690        let make_frame = |start: usize| -> Vec<f32> {
4691            (0..n)
4692                .map(|i| {
4693                    let t = (start + i) as f32 / fs;
4694                    let pi2 = 2.0 * std::f32::consts::PI;
4695                    0.2 * (pi2 * 250.0 * t).sin()
4696                        + 0.2 * (pi2 * 500.0 * t).sin()
4697                        + 0.2 * (pi2 * 1000.0 * t).sin()
4698                })
4699                .collect()
4700        };
4701        let frames: Vec<Vec<f32>> = (0..5).map(|i| make_frame(i * n)).collect();
4702        let decoded = encode_decode_frames(&frames);
4703        // Steady-state frame: index 2 (well past the leading transient).
4704        let orig = &frames[2];
4705        let recon_i16 = &decoded[2];
4706        let recon: Vec<f32> = recon_i16.iter().map(|&s| s as f32 / 32767.0).collect();
4707        let mut sig_e = 0.0_f64;
4708        let mut err_e = 0.0_f64;
4709        for (o, r) in orig.iter().zip(recon.iter()) {
4710            sig_e += (*o as f64).powi(2);
4711            err_e += (*o as f64 - *r as f64).powi(2);
4712        }
4713        let snr_db = 10.0 * (sig_e / err_e.max(1e-30)).log10();
4714        assert!(
4715            snr_db > 10.0,
4716            "multi-tone round-trip SNR too low: {snr_db:.1} dB \
4717             (expected > 10 dB; HCB5-only encoder caps q at ±4 — \
4718             round 49 will widen the codebook selector)"
4719        );
4720    }
4721
4722    /// Silence: encode → decode → assert decoded amplitude is small.
4723    /// HCB5-only encoder always emits a non-zero-padded frame so we
4724    /// expect ε > 0 noise floor — but it should be << peak amplitude.
4725    #[test]
4726    fn encode_frame_pcm_silence_round_trips_to_silence() {
4727        let n = 1920usize;
4728        let frames: Vec<Vec<f32>> = (0..4).map(|_| vec![0.0_f32; n]).collect();
4729        let decoded = encode_decode_frames(&frames);
4730        // Steady-state frame must be effectively silent.
4731        let pcm = &decoded[2];
4732        let peak = pcm.iter().map(|&s| s.abs()).max().unwrap_or(0);
4733        // i16 peak < 50 = -56 dBFS; comfortably below any audible threshold.
4734        assert!(
4735            peak < 50,
4736            "expected silent reconstruction, got peak amplitude {peak}"
4737        );
4738    }
4739
4740    /// Encoder bumps the sequence_counter once per `encode_frame_pcm`
4741    /// call, identical to `encode_frame()`.
4742    #[test]
4743    fn encode_frame_pcm_bumps_sequence_counter() {
4744        let mut enc = Ac4ImsEncoder::new();
4745        assert_eq!(enc.sequence_counter, 0);
4746        let frame = vec![0.0_f32; 1920];
4747        let _ = enc.encode_frame_pcm(&frame);
4748        assert_eq!(enc.sequence_counter, 1);
4749        let _ = enc.encode_frame_pcm(&frame);
4750        assert_eq!(enc.sequence_counter, 2);
4751    }
4752
4753    // ------------------------------------------------------------------
4754    // Round 49 — HCB1..11 codebook selection optimiser + wider max_sfb.
4755    // ------------------------------------------------------------------
4756
4757    fn encode_decode_frames_with_max_sfb(frames: &[Vec<f32>], max_sfb: u32) -> Vec<Vec<i16>> {
4758        use crate::decoder::Ac4Decoder;
4759        use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
4760        let params = CodecParameters::audio(CodecId::new("ac4"));
4761        let mut dec = Ac4Decoder::new(&params);
4762        let mut enc = Ac4ImsEncoder::new();
4763        let mut out: Vec<Vec<i16>> = Vec::with_capacity(frames.len());
4764        for f in frames {
4765            let bytes = enc.encode_frame_pcm_with_max_sfb(f, max_sfb);
4766            let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
4767            dec.send_packet(&pkt).expect("send_packet");
4768            let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
4769                panic!("expected audio frame");
4770            };
4771            assert_eq!(af.samples, 1_920);
4772            assert_eq!(af.data.len(), 1);
4773            let pcm: Vec<i16> = af.data[0]
4774                .chunks_exact(2)
4775                .map(|c| i16::from_le_bytes([c[0], c[1]]))
4776                .collect();
4777            out.push(pcm);
4778        }
4779        out
4780    }
4781
4782    /// White-noise input: encode via the round-49 optimiser, then decode
4783    /// and pull the decoder's reconstructed scaled spectrum
4784    /// (`scaled_spec_primary`) directly out of the substream. Compare
4785    /// bin-for-bin against the encoder's input MDCT spectrum to measure
4786    /// the codebook-selection / quantisation SNR — this isolates the
4787    /// quantiser's noise contribution from the bandlimit / IMDCT
4788    /// reconstruction noise that dominates a time-domain comparison.
4789    ///
4790    /// Round-48 HCB5-only baseline: ~12 dB SNR (|q| ≤ 4 ceiling).
4791    /// Round-49 HCB1..11 with q_target = 12: ≥ 18 dB SNR.
4792    #[test]
4793    fn encode_frame_pcm_white_noise_snr_exceeds_hcb5_only_ceiling() {
4794        use crate::decoder::Ac4Decoder;
4795        use crate::encoder_mdct::EncoderMdctState;
4796        use oxideav_core::{CodecId, CodecParameters, Decoder, Packet, TimeBase};
4797        let n = 1920usize;
4798        let max_sfb = 50u32;
4799        let sfbo = crate::sfb_offset::sfb_offset_48(n as u32).unwrap();
4800        let end_bin = sfbo[max_sfb as usize] as usize;
4801        let make_frame = |seed_off: u64| -> Vec<f32> {
4802            let mut s: u64 = 0xACE4_u64.wrapping_add(seed_off);
4803            (0..n)
4804                .map(|_| {
4805                    s = s
4806                        .wrapping_mul(6364136223846793005)
4807                        .wrapping_add(1442695040888963407);
4808                    let u = (s >> 33) as u32;
4809                    (u as f32 / (1u32 << 31) as f32 - 1.0) * 0.3
4810                })
4811                .collect()
4812        };
4813        // Encode + decode 3 frames; pull the third for steady-state.
4814        let frames: Vec<Vec<f32>> = (0..3).map(|i| make_frame(i as u64 * n as u64)).collect();
4815        let params = CodecParameters::audio(CodecId::new("ac4"));
4816        let mut dec = Ac4Decoder::new(&params);
4817        let mut enc = Ac4ImsEncoder::new();
4818        let mut last_recon_spec: Option<Vec<f32>> = None;
4819        let mut mdct_in = EncoderMdctState::new(n as u32);
4820        let mut last_orig_spec: Option<Vec<f32>> = None;
4821        for f in &frames {
4822            // Mirror the encoder's MDCT on the input.
4823            let orig_coeffs = mdct_in.analyse_frame(f);
4824            last_orig_spec = Some(orig_coeffs.clone());
4825            let bytes = enc.encode_frame_pcm_with_max_sfb(f, max_sfb);
4826            let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
4827            dec.send_packet(&pkt).expect("send_packet");
4828            let _ = dec.receive_frame().expect("receive_frame");
4829            let sub = dec.last_substream.as_ref().unwrap();
4830            let scaled = sub.tools.scaled_spec_primary.as_ref().unwrap().clone();
4831            last_recon_spec = Some(scaled);
4832        }
4833        let orig = last_orig_spec.unwrap();
4834        let recon = last_recon_spec.unwrap();
4835        let mut sig_e = 0.0_f64;
4836        let mut err_e = 0.0_f64;
4837        for k in 0..end_bin {
4838            let o = orig[k] as f64;
4839            let r = recon[k] as f64;
4840            sig_e += o * o;
4841            err_e += (o - r) * (o - r);
4842        }
4843        let snr_db = 10.0 * (sig_e / err_e.max(1e-30)).log10();
4844        eprintln!("ROUND-49 white-noise spectral SNR (HCB1..11 optimiser, q_target=12, max_sfb=50): {snr_db:.1} dB");
4845        assert!(
4846            snr_db > 18.0,
4847            "white-noise spectral SNR did not improve over HCB5-only ceiling: \
4848             {snr_db:.1} dB (expected > 18 dB; round-48 HCB5-only baseline was ~12 dB)"
4849        );
4850    }
4851
4852    /// Wider max_sfb=55: 1 kHz tone reconstruction has ≥80% of input
4853    /// energy preserved (vs ~40% with the round-48 max_sfb=40 default).
4854    #[test]
4855    fn encode_frame_pcm_max_sfb_55_preserves_tone_energy() {
4856        let n = 1920usize;
4857        let fs = 48_000.0_f32;
4858        let f = 1000.0_f32;
4859        let make_frame = |start: usize| -> Vec<f32> {
4860            (0..n)
4861                .map(|i| {
4862                    let t = (start + i) as f32 / fs;
4863                    0.3 * (2.0 * std::f32::consts::PI * f * t).sin()
4864                })
4865                .collect()
4866        };
4867        let frames: Vec<Vec<f32>> = (0..4).map(|i| make_frame(i * n)).collect();
4868        let decoded = encode_decode_frames_with_max_sfb(&frames, 55);
4869        let orig = &frames[2];
4870        let recon_i16 = &decoded[2];
4871        let recon: Vec<f32> = recon_i16.iter().map(|&s| s as f32 / 32767.0).collect();
4872        let orig_e: f64 = orig.iter().map(|&v| (v as f64).powi(2)).sum();
4873        let recon_e: f64 = recon.iter().map(|&v| (v as f64).powi(2)).sum();
4874        let ratio = recon_e / orig_e.max(1e-30);
4875        eprintln!(
4876            "ROUND-49 max_sfb=55 1 kHz tone energy preservation: {:.1}%",
4877            ratio * 100.0
4878        );
4879        assert!(
4880            ratio >= 0.80,
4881            "expected ≥80% energy preservation at max_sfb=55, got {:.1}%",
4882            ratio * 100.0
4883        );
4884    }
4885
4886    /// Backwards compatibility: `encode_frame_pcm` without an explicit
4887    /// max_sfb still uses the round-48 default of 40, and the existing
4888    /// 1 kHz tone fixture still round-trips through the decoder with the
4889    /// optimiser-driven codebook selection enabled.
4890    #[test]
4891    fn encode_frame_pcm_default_max_sfb_still_works() {
4892        let n = 1920usize;
4893        let fs = 48_000.0_f32;
4894        let f = 1000.0_f32;
4895        let make_frame = |start: usize| -> Vec<f32> {
4896            (0..n)
4897                .map(|i| {
4898                    let t = (start + i) as f32 / fs;
4899                    0.3 * (2.0 * std::f32::consts::PI * f * t).sin()
4900                })
4901                .collect()
4902        };
4903        let frames: Vec<Vec<f32>> = (0..4).map(|i| make_frame(i * n)).collect();
4904        let decoded = encode_decode_frames(&frames); // default max_sfb=40
4905        let pcm = &decoded[2];
4906        let peak = pcm.iter().map(|&s| s.abs()).max().unwrap_or(0);
4907        assert!(
4908            peak > 1000,
4909            "expected peak amplitude > 1000 at default max_sfb=40, got {peak}"
4910        );
4911    }
4912
4913    /// Sanity baseline: with the HCB5-only encoder configuration
4914    /// (`q_target = 4`) the white-noise spectral SNR caps near 12 dB.
4915    /// We simulate this via a one-shot helper that uses HCB5 only on
4916    /// every band, mirroring the round-48 build_mono_simple_asf body.
4917    /// This test exists as a benchmark anchor so future regressions
4918    /// against the round-48 baseline are visible at a glance.
4919    #[test]
4920    fn baseline_hcb5_only_white_noise_snr_logs_for_comparison() {
4921        use crate::asf_data::{
4922            dequantise_and_scale, parse_asf_scalefac_data, parse_asf_section_data,
4923            parse_asf_spectral_data,
4924        };
4925        use crate::encoder_asf::{
4926            pick_scalefactor_for_band, single_section, write_scalefac_data, write_sect_len_incr,
4927            write_spectral_data_single_section,
4928        };
4929        use crate::encoder_mdct::EncoderMdctState;
4930        use oxideav_core::bits::{BitReader, BitWriter};
4931
4932        let n = 1920usize;
4933        let max_sfb = 50u32;
4934        let sfbo = crate::sfb_offset::sfb_offset_48(n as u32).unwrap();
4935        let end_bin = sfbo[max_sfb as usize] as usize;
4936        let mut s: u64 = 0xACE4u64;
4937        let pcm: Vec<f32> = (0..n)
4938            .map(|_| {
4939                s = s
4940                    .wrapping_mul(6364136223846793005)
4941                    .wrapping_add(1442695040888963407);
4942                let u = (s >> 33) as u32;
4943                (u as f32 / (1u32 << 31) as f32 - 1.0) * 0.3
4944            })
4945            .collect();
4946        let mut mdct = EncoderMdctState::new(n as u32);
4947        let _ = mdct.analyse_frame(&pcm);
4948        let coeffs = mdct.analyse_frame(&pcm);
4949
4950        // HCB5-only encoder body (round-48 path).
4951        let cb: u8 = 5;
4952        let q_max = 4u32;
4953        let mut qspec = vec![0i32; end_bin];
4954        let mut sf_per_band = vec![100i32; max_sfb as usize];
4955        let mut max_quant_idx = vec![0u32; max_sfb as usize];
4956        for sfb in 0..max_sfb as usize {
4957            let a = sfbo[sfb] as usize;
4958            let b = sfbo[sfb + 1] as usize;
4959            let band = &coeffs[a..b.min(coeffs.len())];
4960            let (sf, q) = pick_scalefactor_for_band(band, q_max);
4961            sf_per_band[sfb] = sf;
4962            for (i, &qi) in q.iter().enumerate() {
4963                qspec[a + i] = qi;
4964                max_quant_idx[sfb] = max_quant_idx[sfb].max(qi.unsigned_abs());
4965            }
4966        }
4967        let mut bw = BitWriter::new();
4968        bw.write_u32(4096, 15);
4969        bw.write_bit(false);
4970        bw.align_to_byte();
4971        bw.write_u32(0, 1);
4972        bw.write_u32(0, 1);
4973        bw.write_bit(true);
4974        let (n_msfb_bits, _, _) = crate::tables::n_msfb_bits_48(n as u32).unwrap();
4975        bw.write_u32(max_sfb, n_msfb_bits);
4976        bw.write_u32(cb as u32, 4);
4977        write_sect_len_incr(&mut bw, max_sfb, 3, 7);
4978        write_spectral_data_single_section(&mut bw, &qspec, sfbo, max_sfb, cb as u32);
4979        let sections = single_section(max_sfb, cb);
4980        write_scalefac_data(
4981            &mut bw,
4982            &sf_per_band,
4983            &sections.sfb_cb,
4984            &max_quant_idx,
4985            max_sfb,
4986        );
4987        bw.write_u32(0, 1);
4988        bw.align_to_byte();
4989        while bw.byte_len() < 4096 {
4990            bw.write_u32(0, 8);
4991        }
4992        let body = bw.finish();
4993
4994        // Walk through parser, then dequantise + compare.
4995        let mut br = BitReader::new(&body);
4996        let _ = br.read_u32(15).unwrap();
4997        let _ = br.read_bit().unwrap();
4998        br.align_to_byte();
4999        let _ = br.read_u32(1).unwrap();
5000        let _ = br.read_u32(1).unwrap();
5001        let _ = br.read_bit().unwrap();
5002        let _ = br.read_u32(n_msfb_bits).unwrap();
5003        let parsed = parse_asf_section_data(&mut br, 0, n as u32, max_sfb).unwrap();
5004        let (qs, mqi) = parse_asf_spectral_data(&mut br, &parsed, sfbo, max_sfb).unwrap();
5005        let sfg = parse_asf_scalefac_data(&mut br, &parsed, &mqi, max_sfb, n as u32).unwrap();
5006        let scaled = dequantise_and_scale(&qs, &sfg, sfbo, max_sfb);
5007
5008        let mut sig_e = 0.0_f64;
5009        let mut err_e = 0.0_f64;
5010        for k in 0..end_bin {
5011            let o = coeffs[k] as f64;
5012            let r = scaled[k] as f64;
5013            sig_e += o * o;
5014            err_e += (o - r) * (o - r);
5015        }
5016        let snr_db = 10.0 * (sig_e / err_e.max(1e-30)).log10();
5017        eprintln!("ROUND-48 baseline white-noise spectral SNR (HCB5-only, q_target=4, max_sfb=50): {snr_db:.1} dB");
5018        // Sanity: round-48 should be in the 8-15 dB range.
5019        assert!(
5020            snr_db < 18.0,
5021            "round-48 HCB5-only baseline unexpectedly high: {snr_db:.1} dB"
5022        );
5023    }
5024
5025    // ------------------------------------------------------------------
5026    // Round 50 — DP section optimiser + SNF emission integration tests.
5027    // ------------------------------------------------------------------
5028
5029    /// SNF-bit-on round-trip: encode a tone+noise input, then verify
5030    /// that the decoded reconstruction has non-zero magnitude in
5031    /// high-frequency bins that the quantiser collapsed to zero. The
5032    /// `b_snf_data_exists` bit must round-trip through the parser
5033    /// without erroring.
5034    #[test]
5035    fn encode_frame_pcm_white_noise_with_snf_fills_zero_quant_bands() {
5036        use crate::decoder::Ac4Decoder;
5037        use oxideav_core::{CodecId, CodecParameters, Decoder, Packet, TimeBase};
5038        let n = 1920usize;
5039        let max_sfb = 55u32;
5040        let sfbo = crate::sfb_offset::sfb_offset_48(n as u32).unwrap();
5041        let end_bin = sfbo[max_sfb as usize] as usize;
5042
5043        // Low-energy white noise — most high bands quantise to cb=0,
5044        // exercising the SNF emission path.
5045        let make_frame = |seed_off: u64| -> Vec<f32> {
5046            let mut s: u64 = 0xACE4_u64.wrapping_add(seed_off);
5047            (0..n)
5048                .map(|_| {
5049                    s = s
5050                        .wrapping_mul(6364136223846793005)
5051                        .wrapping_add(1442695040888963407);
5052                    let u = (s >> 33) as u32;
5053                    (u as f32 / (1u32 << 31) as f32 - 1.0) * 0.05 // low energy
5054                })
5055                .collect()
5056        };
5057        let frames: Vec<Vec<f32>> = (0..3).map(|i| make_frame(i as u64 * n as u64)).collect();
5058        let params = CodecParameters::audio(CodecId::new("ac4"));
5059        let mut dec = Ac4Decoder::new(&params);
5060        let mut enc = Ac4ImsEncoder::new();
5061        let mut last_recon: Option<Vec<f32>> = None;
5062        for f in &frames {
5063            let bytes = enc.encode_frame_pcm_with_max_sfb(f, max_sfb);
5064            let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
5065            dec.send_packet(&pkt).expect("send_packet");
5066            let _ = dec.receive_frame().expect("receive_frame");
5067            let sub = dec.last_substream.as_ref().unwrap();
5068            let scaled = sub.tools.scaled_spec_primary.as_ref().unwrap().clone();
5069            last_recon = Some(scaled);
5070        }
5071        let recon = last_recon.unwrap();
5072        // Count non-zero bins in the recon — with SNF on, even bands that
5073        // collapsed to cb=0 should have non-zero magnitude from injected
5074        // noise (clamped by what the SNF index range allows).
5075        let nonzero = recon[..end_bin].iter().filter(|&&v| v.abs() > 0.0).count();
5076        // We don't insist on every bin being non-zero (some bands may have
5077        // SNF idx 0 = "no fill"); the assertion is that the bitstream
5078        // round-trips without error and decodes to a non-silent spectrum.
5079        assert!(
5080            nonzero > 0,
5081            "expected at least one non-zero bin in SNF reconstruction, got {nonzero}"
5082        );
5083    }
5084
5085    /// SNF integration: SNF-on bitstream parses cleanly through the
5086    /// existing decoder. This is the smoke test for the new emission
5087    /// path — it MUST not break decode of non-SNF frames either.
5088    #[test]
5089    fn encode_frame_pcm_silence_with_snf_off_round_trips() {
5090        // Pure silence input: no band has measurable energy → SNF should
5091        // be `None` → b_snf_data_exists = 0 in the bitstream.
5092        let n = 1920usize;
5093        let frames: Vec<Vec<f32>> = (0..2).map(|_| vec![0.0_f32; n]).collect();
5094        let decoded = encode_decode_frames_with_max_sfb(&frames, 50);
5095        let pcm = &decoded[1];
5096        let peak = pcm.iter().map(|&s| s.abs()).max().unwrap_or(0);
5097        // Silence input → silence output (no SNF fill since no energy).
5098        assert_eq!(
5099            peak, 0,
5100            "silence + SNF-off should decode to silence, peak={peak}"
5101        );
5102    }
5103
5104    /// max_sfb wider than the round-48 default: encoder emits a frame
5105    /// the decoder parses without erroring.
5106    #[test]
5107    fn encode_frame_pcm_max_sfb_50_round_trips() {
5108        let n = 1920usize;
5109        let fs = 48_000.0_f32;
5110        let make_frame = |start: usize| -> Vec<f32> {
5111            (0..n)
5112                .map(|i| {
5113                    let t = (start + i) as f32 / fs;
5114                    let pi2 = 2.0 * std::f32::consts::PI;
5115                    // Tones across the wider band: 1 kHz + 8 kHz.
5116                    0.2 * (pi2 * 1000.0 * t).sin() + 0.2 * (pi2 * 8000.0 * t).sin()
5117                })
5118                .collect()
5119        };
5120        let frames: Vec<Vec<f32>> = (0..4).map(|i| make_frame(i * n)).collect();
5121        let decoded = encode_decode_frames_with_max_sfb(&frames, 50);
5122        // Steady-state frame must be substantially non-silent.
5123        let pcm = &decoded[2];
5124        let nonzero = pcm.iter().filter(|&&s| s != 0).count();
5125        assert!(nonzero > 100, "expected non-silent recon, got {nonzero}");
5126    }
5127
5128    /// Round 52 sanity: identical L=R PCM → MDCT spectra are bit-identical
5129    /// → per-SFB energy-weighted correlation is exactly 1.0; the
5130    /// dispatcher would route this frame to Path B (joint M/S).
5131    #[test]
5132    fn round52_correlation_identical_channels_is_one() {
5133        use crate::encoder_asf::average_per_sfb_correlation;
5134        use crate::encoder_mdct::EncoderMdctState;
5135        let n = 1920usize;
5136        let fs = 48_000.0_f32;
5137        let make_frame = |start: usize| -> Vec<f32> {
5138            (0..n)
5139                .map(|i| {
5140                    let t = (start + i) as f32 / fs;
5141                    0.3 * (2.0 * std::f32::consts::PI * 440.0 * t).sin()
5142                })
5143                .collect()
5144        };
5145        let mut mdct_l = EncoderMdctState::new(n as u32);
5146        let mut mdct_r = EncoderMdctState::new(n as u32);
5147        let mut rhos: Vec<f32> = Vec::new();
5148        for i in 0..3 {
5149            let f = make_frame(i * n);
5150            let cl = mdct_l.analyse_frame(&f);
5151            let cr = mdct_r.analyse_frame(&f);
5152            let rho = average_per_sfb_correlation(n as u32, 40, &cl, &cr);
5153            rhos.push(rho);
5154        }
5155        for (i, &rho) in rhos.iter().enumerate() {
5156            assert!(
5157                (rho - 1.0).abs() < 1e-4,
5158                "frame {i} rho expected 1.0, got {rho}"
5159            );
5160        }
5161    }
5162
5163    /// Round 52 sanity: 440 Hz L + 660 Hz R independent channels have
5164    /// well-separated MDCT spectra → energy-weighted per-SFB correlation
5165    /// falls below the 0.7 dispatch threshold; Path A is chosen.
5166    #[test]
5167    fn round52_correlation_independent_tones_below_threshold() {
5168        use crate::encoder_asf::average_per_sfb_correlation;
5169        use crate::encoder_mdct::EncoderMdctState;
5170        let n = 1920usize;
5171        let fs = 48_000.0_f32;
5172        let make_frame = |freq: f32, start: usize| -> Vec<f32> {
5173            (0..n)
5174                .map(|i| {
5175                    let t = (start + i) as f32 / fs;
5176                    0.3 * (2.0 * std::f32::consts::PI * freq * t).sin()
5177                })
5178                .collect()
5179        };
5180        let mut mdct_l = EncoderMdctState::new(n as u32);
5181        let mut mdct_r = EncoderMdctState::new(n as u32);
5182        let mut rhos: Vec<f32> = Vec::new();
5183        for i in 0..3 {
5184            let fl = make_frame(440.0, i * n);
5185            let fr = make_frame(660.0, i * n);
5186            let cl = mdct_l.analyse_frame(&fl);
5187            let cr = mdct_r.analyse_frame(&fr);
5188            rhos.push(average_per_sfb_correlation(n as u32, 40, &cl, &cr));
5189        }
5190        for (i, &rho) in rhos.iter().enumerate() {
5191            assert!(rho.abs() < 0.6, "frame {i} rho expected < 0.6, got {rho}");
5192        }
5193    }
5194
5195    // ------------------------------------------------------------------
5196    // Round 51 — Stereo SIMPLE/ASF split-MDCT (Path A, 2× SCE) tests.
5197    // ------------------------------------------------------------------
5198
5199    /// Helper: encode a sequence of stereo PCM frames (each `(L, R)`)
5200    /// through `encode_frame_pcm_stereo`, then decode them via
5201    /// `Ac4Decoder` and return per-frame deinterleaved `(L, R)` i16 PCM.
5202    fn encode_decode_stereo_frames(
5203        frames_lr: &[(Vec<f32>, Vec<f32>)],
5204    ) -> Vec<(Vec<i16>, Vec<i16>)> {
5205        use crate::decoder::Ac4Decoder;
5206        use oxideav_core::{CodecId, CodecParameters, Decoder, Frame, Packet, TimeBase};
5207        let params = CodecParameters::audio(CodecId::new("ac4"));
5208        let mut dec = Ac4Decoder::new(&params);
5209        let mut enc = Ac4ImsEncoder::new();
5210        let mut out: Vec<(Vec<i16>, Vec<i16>)> = Vec::with_capacity(frames_lr.len());
5211        for (l, r) in frames_lr {
5212            let bytes = enc.encode_frame_pcm_stereo(l, r);
5213            let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
5214            dec.send_packet(&pkt).expect("send_packet");
5215            let Frame::Audio(af) = dec.receive_frame().expect("receive_frame") else {
5216                panic!("expected audio frame");
5217            };
5218            assert_eq!(af.samples, 1_920);
5219            assert_eq!(af.data.len(), 1);
5220            // Stereo S16 interleaved: 1920 samples × 2 ch × 2 bytes.
5221            assert_eq!(af.data[0].len(), 1_920 * 2 * 2);
5222            let buf = &af.data[0];
5223            let mut pcm_l: Vec<i16> = Vec::with_capacity(1_920);
5224            let mut pcm_r: Vec<i16> = Vec::with_capacity(1_920);
5225            for i in 0..1_920usize {
5226                let off_l = i * 4;
5227                let off_r = off_l + 2;
5228                pcm_l.push(i16::from_le_bytes([buf[off_l], buf[off_l + 1]]));
5229                pcm_r.push(i16::from_le_bytes([buf[off_r], buf[off_r + 1]]));
5230            }
5231            out.push((pcm_l, pcm_r));
5232        }
5233        out
5234    }
5235
5236    /// Stereo encoder bumps sequence_counter once per frame, just like
5237    /// the mono path.
5238    #[test]
5239    fn encode_frame_pcm_stereo_bumps_sequence_counter() {
5240        let mut enc = Ac4ImsEncoder::new();
5241        assert_eq!(enc.sequence_counter, 0);
5242        let frame = vec![0.0_f32; 1920];
5243        let _ = enc.encode_frame_pcm_stereo(&frame, &frame);
5244        assert_eq!(enc.sequence_counter, 1);
5245        let _ = enc.encode_frame_pcm_stereo(&frame, &frame);
5246        assert_eq!(enc.sequence_counter, 2);
5247    }
5248
5249    /// Stereo encoder produces a frame whose TOC declares 2 channels and
5250    /// whose decoded PCM layout is stereo (1920 × 2 × 2 bytes).
5251    #[test]
5252    fn encode_frame_pcm_stereo_produces_stereo_layout_pcm() {
5253        let n = 1920usize;
5254        let frames: Vec<(Vec<f32>, Vec<f32>)> = (0..2)
5255            .map(|_| (vec![0.0_f32; n], vec![0.0_f32; n]))
5256            .collect();
5257        let decoded = encode_decode_stereo_frames(&frames);
5258        // Both decoded frames have the stereo S16 byte layout.
5259        for (l, r) in &decoded {
5260            assert_eq!(l.len(), 1_920);
5261            assert_eq!(r.len(), 1_920);
5262        }
5263    }
5264
5265    /// Stereo encoder roundtrip: decoder produces non-silent PCM in
5266    /// both channels with peak amplitudes reflecting the input level.
5267    #[test]
5268    fn encode_frame_pcm_stereo_440hz_steady_state_nonsilent_both_channels() {
5269        let n = 1920usize;
5270        let fs = 48_000.0_f32;
5271        let make_frame = |start: usize| -> Vec<f32> {
5272            (0..n)
5273                .map(|i| {
5274                    let t = (start + i) as f32 / fs;
5275                    0.3 * (2.0 * std::f32::consts::PI * 440.0 * t).sin()
5276                })
5277                .collect()
5278        };
5279        let frames_lr: Vec<(Vec<f32>, Vec<f32>)> = (0..5)
5280            .map(|i| (make_frame(i * n), make_frame(i * n)))
5281            .collect();
5282        let decoded = encode_decode_stereo_frames(&frames_lr);
5283        let (l, r) = &decoded[2];
5284        let nz_l = l.iter().filter(|&&s| s != 0).count();
5285        let nz_r = r.iter().filter(|&&s| s != 0).count();
5286        let peak_l = l.iter().map(|&s| s.abs()).max().unwrap_or(0);
5287        let peak_r = r.iter().map(|&s| s.abs()).max().unwrap_or(0);
5288        assert!(nz_l > 100, "L too few non-zero samples: {nz_l}");
5289        assert!(nz_r > 100, "R too few non-zero samples: {nz_r}");
5290        // 0.3 input amplitude → ~0.3 * 32767 ≈ 9830 i16 peak. The
5291        // encoder/decoder lossy round-trip stays comfortably above 1000.
5292        assert!(peak_l > 1000, "L peak too low: {peak_l}");
5293        assert!(peak_r > 1000, "R peak too low: {peak_r}");
5294    }
5295
5296    /// Stereo encoder TOC declares 2 channels and the substream parser
5297    /// surfaces both per-channel scaled spectra. Round 52 update: with
5298    /// identical channels (L=R), the cross-channel correlation is 1.0 so
5299    /// the dispatcher routes the frame through Path B (joint M/S CPE,
5300    /// `b_enable_mdct_stereo_proc == 1`). Force the split-MDCT path so
5301    /// this structural test continues to exercise Path A.
5302    #[test]
5303    fn encode_frame_pcm_stereo_substream_parses() {
5304        use crate::decoder::Ac4Decoder;
5305        use oxideav_core::{CodecId, CodecParameters, Decoder, Packet, TimeBase};
5306        let mut enc = Ac4ImsEncoder::new();
5307        let n = 1920usize;
5308        let fs = 48_000.0_f32;
5309        let frame: Vec<f32> = (0..n)
5310            .map(|i| {
5311                let t = i as f32 / fs;
5312                0.3 * (2.0 * std::f32::consts::PI * 440.0 * t).sin()
5313            })
5314            .collect();
5315        let bytes = enc.encode_frame_pcm_stereo_split_with_max_sfb(&frame, &frame, 40);
5316        let info = crate::toc::parse_ac4_toc(&bytes).expect("parse_ac4_toc");
5317        assert_eq!(info.channels, 2);
5318        let params = CodecParameters::audio(CodecId::new("ac4"));
5319        let mut dec = Ac4Decoder::new(&params);
5320        let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
5321        dec.send_packet(&pkt).expect("send_packet");
5322        let _ = dec.receive_frame().expect("receive_frame");
5323        let sub = dec.last_substream.as_ref().expect("substream parsed");
5324        // SIMPLE stereo mode + b_enable_mdct_stereo_proc = 0 (split-MDCT
5325        // path forced by `encode_frame_pcm_stereo_split_with_max_sfb`).
5326        // Both channels' spectra populated.
5327        assert!(matches!(
5328            sub.tools.stereo_mode,
5329            Some(crate::asf::StereoCodecMode::Simple)
5330        ));
5331        assert!(!sub.tools.mdct_stereo_proc);
5332        assert!(sub.tools.scaled_spec_primary.is_some());
5333        assert!(sub.tools.scaled_spec_secondary.is_some());
5334    }
5335
5336    /// Round 48 stereo SNR target: 440 Hz tone on L + 440 Hz tone on R
5337    /// (identical content) round-trips with **spectral SNR ≥ 20 dB** on
5338    /// the steady-state frame for both channels.
5339    ///
5340    /// Spectral SNR is measured by mirroring the encoder's forward MDCT
5341    /// over the input PCM and comparing the input MDCT spectrum bin-for-
5342    /// bin against the decoder's reconstructed `scaled_spec_*`. This
5343    /// isolates the encoder's quantisation contribution from the IMDCT/
5344    /// KBD overlap-add reconstruction noise (which dominates a time-
5345    /// domain comparison since the IMDCT introduces a half-frame phase
5346    /// shift between the original and reconstructed waveforms even for
5347    /// perfect-reconstruction transforms — same convention used by the
5348    /// round-49 white-noise test
5349    /// `encode_frame_pcm_white_noise_snr_exceeds_hcb5_only_ceiling`).
5350    #[test]
5351    fn encode_frame_pcm_stereo_440hz_both_channels_snr_exceeds_20db() {
5352        use crate::decoder::Ac4Decoder;
5353        use crate::encoder_mdct::EncoderMdctState;
5354        use oxideav_core::{CodecId, CodecParameters, Decoder, Packet, TimeBase};
5355        let n = 1920usize;
5356        let fs = 48_000.0_f32;
5357        let make_frame = |start: usize| -> Vec<f32> {
5358            (0..n)
5359                .map(|i| {
5360                    let t = (start + i) as f32 / fs;
5361                    0.3 * (2.0 * std::f32::consts::PI * 440.0 * t).sin()
5362                })
5363                .collect()
5364        };
5365        let frames: Vec<Vec<f32>> = (0..3).map(|i| make_frame(i * n)).collect();
5366        // Mirror the encoder's MDCT on the input for both channels (same
5367        // PCM here, so we only need one mirror state).
5368        let mut mdct_in = EncoderMdctState::new(n as u32);
5369        let mut last_input_spec: Option<Vec<f32>> = None;
5370        let params = CodecParameters::audio(CodecId::new("ac4"));
5371        let mut dec = Ac4Decoder::new(&params);
5372        let mut enc = Ac4ImsEncoder::new();
5373        let mut last_pri: Option<Vec<f32>> = None;
5374        let mut last_sec: Option<Vec<f32>> = None;
5375        for f in &frames {
5376            let input_coeffs = mdct_in.analyse_frame(f);
5377            last_input_spec = Some(input_coeffs);
5378            let bytes = enc.encode_frame_pcm_stereo(f, f);
5379            let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
5380            dec.send_packet(&pkt).expect("send_packet");
5381            let _ = dec.receive_frame().expect("receive_frame");
5382            let sub = dec.last_substream.as_ref().unwrap();
5383            last_pri = sub.tools.scaled_spec_primary.clone();
5384            last_sec = sub.tools.scaled_spec_secondary.clone();
5385        }
5386        let input = last_input_spec.unwrap();
5387        let pri = last_pri.unwrap();
5388        let sec = last_sec.unwrap();
5389        let snr = |orig: &[f32], recon: &[f32]| -> f64 {
5390            let mut sig_e = 0.0_f64;
5391            let mut err_e = 0.0_f64;
5392            let n_compare = orig.len().min(recon.len());
5393            for k in 0..n_compare {
5394                let o = orig[k] as f64;
5395                let r = recon[k] as f64;
5396                sig_e += o * o;
5397                err_e += (o - r) * (o - r);
5398            }
5399            10.0 * (sig_e / err_e.max(1e-30)).log10()
5400        };
5401        let snr_l = snr(&input, &pri);
5402        let snr_r = snr(&input, &sec);
5403        eprintln!(
5404            "ROUND-51 stereo 440Hz L+R spectral SNR: SNR_L = {snr_l:.1} dB, SNR_R = {snr_r:.1} dB"
5405        );
5406        assert!(
5407            snr_l > 20.0,
5408            "L channel spectral SNR too low: {snr_l:.1} dB (expected > 20 dB)"
5409        );
5410        assert!(
5411            snr_r > 20.0,
5412            "R channel spectral SNR too low: {snr_r:.1} dB (expected > 20 dB)"
5413        );
5414    }
5415
5416    /// Round 48 stereo independence target: 440 Hz tone on L + 660 Hz
5417    /// tone on R round-trips with **spectral SNR ≥ 20 dB** on the
5418    /// steady-state frame for both channels (proves channels are encoded
5419    /// independently — no cross-channel bleed). See the docstring on
5420    /// [`encode_frame_pcm_stereo_440hz_both_channels_snr_exceeds_20db`]
5421    /// for why we compare in the spectral domain.
5422    #[test]
5423    fn encode_frame_pcm_stereo_440l_660r_independent_channels_snr_exceeds_20db() {
5424        use crate::decoder::Ac4Decoder;
5425        use crate::encoder_mdct::EncoderMdctState;
5426        use oxideav_core::{CodecId, CodecParameters, Decoder, Packet, TimeBase};
5427        let n = 1920usize;
5428        let fs = 48_000.0_f32;
5429        let make_frame_at = |freq: f32| -> Box<dyn Fn(usize) -> Vec<f32>> {
5430            Box::new(move |start: usize| -> Vec<f32> {
5431                (0..n)
5432                    .map(|i| {
5433                        let t = (start + i) as f32 / fs;
5434                        0.3 * (2.0 * std::f32::consts::PI * freq * t).sin()
5435                    })
5436                    .collect()
5437            })
5438        };
5439        let make_l = make_frame_at(440.0);
5440        let make_r = make_frame_at(660.0);
5441        let frames_lr: Vec<(Vec<f32>, Vec<f32>)> =
5442            (0..3).map(|i| (make_l(i * n), make_r(i * n))).collect();
5443        // Mirror MDCT on each channel's input independently.
5444        let mut mdct_l = EncoderMdctState::new(n as u32);
5445        let mut mdct_r = EncoderMdctState::new(n as u32);
5446        let mut last_in_l: Option<Vec<f32>> = None;
5447        let mut last_in_r: Option<Vec<f32>> = None;
5448        let params = CodecParameters::audio(CodecId::new("ac4"));
5449        let mut dec = Ac4Decoder::new(&params);
5450        let mut enc = Ac4ImsEncoder::new();
5451        let mut last_pri: Option<Vec<f32>> = None;
5452        let mut last_sec: Option<Vec<f32>> = None;
5453        for (l, r) in &frames_lr {
5454            last_in_l = Some(mdct_l.analyse_frame(l));
5455            last_in_r = Some(mdct_r.analyse_frame(r));
5456            let bytes = enc.encode_frame_pcm_stereo(l, r);
5457            let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes);
5458            dec.send_packet(&pkt).expect("send_packet");
5459            let _ = dec.receive_frame().expect("receive_frame");
5460            let sub = dec.last_substream.as_ref().unwrap();
5461            last_pri = sub.tools.scaled_spec_primary.clone();
5462            last_sec = sub.tools.scaled_spec_secondary.clone();
5463        }
5464        let in_l = last_in_l.unwrap();
5465        let in_r = last_in_r.unwrap();
5466        let pri = last_pri.unwrap();
5467        let sec = last_sec.unwrap();
5468        let snr = |orig: &[f32], recon: &[f32]| -> f64 {
5469            let mut sig_e = 0.0_f64;
5470            let mut err_e = 0.0_f64;
5471            let n_compare = orig.len().min(recon.len());
5472            for k in 0..n_compare {
5473                let o = orig[k] as f64;
5474                let r = recon[k] as f64;
5475                sig_e += o * o;
5476                err_e += (o - r) * (o - r);
5477            }
5478            10.0 * (sig_e / err_e.max(1e-30)).log10()
5479        };
5480        let snr_l = snr(&in_l, &pri);
5481        let snr_r = snr(&in_r, &sec);
5482        eprintln!(
5483            "ROUND-51 stereo 440L+660R independent spectral SNR: SNR_L = {snr_l:.1} dB, SNR_R = {snr_r:.1} dB"
5484        );
5485        assert!(
5486            snr_l > 20.0,
5487            "L (440 Hz) channel spectral SNR too low: {snr_l:.1} dB (expected > 20 dB)"
5488        );
5489        assert!(
5490            snr_r > 20.0,
5491            "R (660 Hz) channel spectral SNR too low: {snr_r:.1} dB (expected > 20 dB)"
5492        );
5493        // Independence sanity check: L and R reconstructions should differ
5494        // (different input frequencies → different waveforms in the
5495        // spectrum).
5496        let differs = pri
5497            .iter()
5498            .zip(sec.iter())
5499            .filter(|(a, b)| (*a - *b).abs() > 0.01)
5500            .count();
5501        assert!(
5502            differs > 10,
5503            "L and R reconstructed spectra should differ for independent tones (got {differs} diffs)"
5504        );
5505    }
5506}