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(¶ms);
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(¶ms);
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(¶ms);
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(¶ms);
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(¶ms);
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(¶ms);
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(¶ms);
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 §ions.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(¶ms);
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(¶ms);
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(¶ms);
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(¶ms);
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(¶ms);
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}