Skip to content

v0.0.6

Choose a tag to compare

@MagicalTux MagicalTux released this 30 May 03:23
· 32 commits to master since this release

Other

  • ac4 r190: close ASPX_ACPL_1 desync — fix aspx_framing() FIXFIX prefix
  • ac4 round 187 — pin ACPL_1 residual / α-β desync follow-up
  • round 181 — close r128 alpha_q desync at parser indexing + aspx_data_2ch SIGNAL band count
  • round 174 — fix ALPHA / BETA3 F0 cb_off (latent #1121 desync)
  • round 144 — real per-band α + β extraction for 5_X ASPX_ACPL_2
  • round 139 — 7.1-with-LFE ACPL_1 real per-band α + β
  • round 135 — real per-band α + β extraction for 7_X ASPX_ACPL_1
  • round 132 — real per-band β extraction in ACPL_1 5.0 encoder
  • round 128 — real per-band α extraction in ACPL_1 5.0 encoder
  • round 125 — 7.0 (3/4/0) SIMPLE/Cfg3Five multichannel encoder
  • round 118 — 7.0/7.1 SIMPLE/ASPX_ACPL_1 multichannel encoder
  • Round 114: 7.1 (3/4/0.1) SIMPLE/ASPX_ACPL_2 multichannel encoder (LFE)
  • Round 107: 7.0 SIMPLE/ASPX_ACPL_2 multichannel encoder
  • round 103 — 5_X SIMPLE/ASPX_ACPL_1 multichannel encoder path
  • round 100 — 5_X SIMPLE/ASPX_ACPL_2 multichannel encoder path
  • ac4 round 95: 5_X SIMPLE/ASPX_ACPL_3 multichannel encoder path
  • ac4 round 91: 7.1 (3/4/0.1) SIMPLE/Cfg3Five encoder (7 SCE + LFE)
  • ac4 round 80: 5.1 SIMPLE/Cfg3Five encoder (5 SCE + LFE) + decoder LFE PCM render
  • ac4 round 74: 5.0 SIMPLE/Cfg3Five multichannel forward analysis (5 SCE)
  • ac4 round 52: joint M/S CPE (Path B, b_enable_mdct_stereo_proc=1)
  • ac4 round 51: stereo SIMPLE/ASF split-MDCT (Path A: 2x SCE) encoder

Fixed

  • Round 190 — close the 5_X ASPX_ACPL_1 desync the r187 tests
    pinned.
    Two minimal A-SPX writers
    ([crate::encoder_acpl3::write_aspx_data_2ch_minimal] and
    [write_aspx_data_1ch_minimal]) emitted aspx_int_class = FIXFIX
    as the wrong prefix code: 0b11 (2 bits) instead of 0b0 (1 bit)
    per ETSI TS 103 190-1 Table 126. The decoder's
    [crate::aspx::AspxIntClass::read] correctly walks the prefix —
    0 → FixFix, 10 → FixVar, 110 / 111 → VarFix / VarVar — so
    the writer's 11 start signalled the parser to read the VarFix /
    VarVar branch instead. For our config that put the parser in the
    VarFix branch with b_iframe = 1: it then read
    var_bord_left (2 b), num_rel_left (2 b — num_aspx_timeslots = 15 > 8 makes Note-1 fields 2-bit wide), and tsg_ptr (2 b).
    Net: parser consumed 9 bits in the framing where the writer
    only emitted 3 bits, a 6-bit upstream drift that the
    silence / L-only / Ls-only test paths masked (α / β quantised to 0
    ⇒ the acpl_data_1ch body shape was constant minimum-cost on each
    side and the num_param_sets_cod bit positions on both sides
    sampled 0 within the long run of zero codewords). With non-zero
    α / β the codewords shift, the pair-1 num_param_sets_cod bit
    position lands on a 1, and pair1 reads num_param_sets = 2
    the r187 symptom. Fix is one line per writer: emit
    bw.write_bit(false) for the FIXFIX prefix, matching Table 126.
    • The r187 test #4 (acpl1_combined_l_and_ls_pair1_currently_misaligns)
      was renamed to acpl1_full_round_trips_with_aligned_pair_lengths
      and its assertion flipped from assert_eq!(n1, 2) (pinned
      misalignment) to assert_eq!(n1, 1) (post-fix). All four
      combinations now round-trip with
      pair0.num_param_sets = pair1.num_param_sets = 1.
    • Total tests 784 (unchanged from r187 — r190 fixed the third pin
      in place rather than adding new ones; the bit-level diagnosis is
      carried in the test file's module doc-comment instead).

Added

  • Round 187 — characterisation tests pinning the remaining
    5_X ASPX_ACPL_1 residual / α-β-writer desync the r181 follow-up
    flagged.
    Four end-to-end pinning tests in
    tests/round187_acpl1_residual_desync_characterization.rs sweep
    the encoder's encode_frame_pcm_5_0_acpl1_real_alpha_beta across
    four input combinations and assert the decoder's recovered
    acpl_data_1ch_pair[0/1].framing.num_param_sets so the next round
    can iterate on the residual-layer / α-β writers without regressing
    the aligned silence / L-only / Ls-only paths.
    • Silence (all-zero PCM) → both pair slots resolve
      num_param_sets = 1.
    • L-carrier-only (Ls = Rs = 0) → both pair slots resolve
      num_param_sets = 1; the write_two_channel_data carrier writer
      is exercised non-trivially while α / β stay quantised to 0
      (correlation Σ L · Ls = 0 ⇒ α extractor returns 0; surround
      energy E[Ls²] = 0 ⇒ β extractor returns 0).
    • Ls-residual-only (L = R = 0) → both pair slots still resolve
      num_param_sets = 1; the write_acpl_1_residual_layer joint-MDCT
      residual writer is exercised non-trivially with max_sfb_master
      non-zero band budget but α / β stay 0 because carrier energy is 0.
    • Combined L-carrier + Ls-residual (L = 0.5, Ls = 0.05) →
      pair0 still resolves num_param_sets = 1, but pair1 drifts to
      num_param_sets = 2. The pin captures this as the currently
      expected
      behaviour so the next round's residual-layer fix can
      flip the assertion back to 1 once aligned.
    • Diagnostic narrative in the test file's module doc-comment
      triangulates the bug surface: the writer→parser pairs for
      write_acpl_data_1ch_real_alpha_betaparse_acpl_data_1ch
      are bit-exact in isolation (pinned by
      round181_alpha_desync_fix::standalone_*); back-to-back
      invocations into the same BitWriter without byte alignment
      between them also round-trip cleanly. The drift therefore sits
      upstream of pair0 — either in the joint-MDCT residual writer
      (write_acpl_1_residual_layer) vs the inline residual walk
      inside parse_aspx_acpl_1_2_inner_body's ASPX_ACPL_1 branch, or
      in the two_channel_data() L/R carrier writer vs
      parse_two_channel_data — when L and Ls are simultaneously
      non-trivial. Total tests 784 (was 780).

Fixed

  • Round 181 — A-CPL acpl_huff_data() Pseudocode-121 indexing +
    aspx_data_2ch() SIGNAL band count.
    Closes the user's "alpha_q
    desync" follow-up the round-174 ALPHA / BETA3 F0 cb_off fix
    deferred. Two distinct layers were involved.

    • Layer 1 — §4.2.13.7 Table 65 / §5.7.7.7 Pseudocode 121 parser
      indexing.
      Pre-r181 [crate::acpl::parse_acpl_huff_data] packed
      the (num_param_bands - start_band) Huffman-decoded values into
      a vector starting at index 0. Per the spec the same array is the
      input to Pseudocode 121's for (i = 0; i < num_bands; i++)
      accumulation — so positions [0..start_band) are zero (the
      encoder did not transmit those bands) and the F0 codeword lands at
      values[start_band]. The packed-from-0 layout silently shifted
      the §5.7.7.7 DIFF_FREQ accumulation by start_band parameter
      bands for the 5_X SIMPLE/ASPX_ACPL_1 PARTIAL path
      (acpl_qmf_band > 0, start_band > 0). The r181 fix rewrites
      [crate::acpl::parse_acpl_huff_data] to return a length-
      num_param_bands vector indexed by full param-band number — the
      spec-aligned acpl_<SET>[ps][i] shape Pseudocode 121 reads. The
      [AcplHuffParam`] doc comment now spells out the new indexing
      contract.
    • Layer 2 — §4.2.12.4 Table 52 aspx_data_2ch() SIGNAL band
      count.
      Per ETSI TS 103 190-1 §4.3.10.4.9 (Table 124 NOTE 3)
      aspx_ec_data(SIGNAL, …) reads num_sbg_sig_lowres SIGNAL bands
      when the corresponding aspx_freq_res[env] bit was emitted as 0
      and num_sbg_sig_highres when it was 1 or absent (when
      freq_res_mode != Signalled the encoder writes no in-band
      aspx_freq_res bit and the decoder's
      freq_res.get(env).copied().unwrap_or(true) fallback selects
      high-res). Pre-r181 [crate::encoder_acpl3::write_aspx_data_2ch_minimal]
      hard-coded num_sbg_sig_lowres regardless — so for the
      encoder's default freq_res_mode = DurationDependent config
      (20-band high-res vs 10-band low-res, no in-band freq_res bit)
      the writer emitted 10 SIGNAL F0+DF codewords per channel while
      the parser read 20. The 20-vs-10 mismatch buried every
      subsequent acpl_data_1ch() α / β codeword in trailing
      zero-padding, recovered as length-num_param_bands all-zero
      rows. r181 keys the SIGNAL band count off cfg.signals_freq_res()
      matching the writer's own freq_res-bit gate.
    • 4 new round-181 unit / integration tests in
      tests/round181_alpha_desync_fix.rs:
      [standalone_alpha_writer_round_trips_through_parser] (Layer 1,
      standalone writer→parser→differential_decode),
      [parser_values_are_indexed_by_full_param_band_number] (Layer 1,
      structural — confirms values[0..start_band) == 0 and
      values[start_band..] carries the F0 + DF accumulation),
      [end_to_end_acpl2_asymmetric_surround_recovers_nonzero_alpha]
      (Layer 2 — encode 5.0 ASPX_ACPL_2 with asymmetric L/Ls energy,
      decode, assert acpl_data_1ch_pair[0/1].alpha1[0].values
      carries non-zero entries),
      [end_to_end_acpl2_silence_still_round_trips] (Layer 2 regression
      guard — silence input still yields all-zero α/β). Total tests
      780 (was 776).
    • The r132 acpl_data_1ch_real_alpha_beta_round_trips_byte_exact
      test is re-shaped to apply the spec-aligned Pseudocode 121 DIFF_FREQ
      accumulation directly on the parser's length-num_param_bands
      output (instead of cumulating the pre-r181 packed
      (num_bands - start_band)-length slice).
    • The 5_X SIMPLE/ASPX_ACPL_1 PARTIAL end-to-end path retains a
      separate joint-MDCT residual-layer alignment issue (the residual
      sf_data writer and the decoder's decode_asf_long_mono_body_with_max_sfb
      appear to read different total bit counts on non-trivial inputs)
      that the r181 ACPL_2 end-to-end tests do not exercise. Tracking
      as the remaining "alpha_q desync" follow-up — the structural
      Layer 1 + Layer 2 fixes are independent of it and land here.
  • Round 174 — ALPHA / BETA3 F0 codebook cb_off corrected per ETSI
    TS 103 190-1 §A.3 Tables A.34 / A.35 (ALPHA) and A.46 / A.47 (BETA3).

    • Pre-fix cb_off = 0 for ALPHA / BETA3 F0 conflicted with the §5.7.7.7
      Pseudocode 121 differential-decoder contract — dequantize_alpha_index
      re-adds the signed-lane offset (+8 Coarse / +16 Fine for ALPHA),
      expecting the Huffman pipeline to deliver the signed quantised lane
      alpha_q ∈ [-N/2, +N/2]. With cb_off = 0 the F0 codeword
      symbol_index was returned raw (unsigned 0..N), shifting every
      dequant lookup by N/2 lanes.
    • Symptom: the round-128 / 132 / 135 / 139 / 144 real-α / α+β encoder
      paths populated alpha_q_per_band correctly via quantise_alpha
      (which already returned signed lanes via its own cb_off = 8 / 16)
      but write_acpl_alpha_f0_value then wrote symbol_index = alpha_q
      instead of symbol_index = alpha_q + 8 / 16. For alpha_q = 0 the
      writer picked the most expensive lane (10 / 12 bits) instead of
      the 1-bit symmetric peak; for negative alpha_q the writer clamped
      to lane 0; positive alpha_q round-tripped accidentally for
      sufficiently small magnitudes via the raw symbol_index lookup. The
      decoder dequant lane shifted by cb_off, producing wrong
      dequantised α magnitudes.
    • Fix: set cb_off = 8 (Coarse) / 16 (Fine) for ALPHA F0 in both
      [crate::acpl::get_acpl_hcb] (decoder) and the matching encoder-
      local acpl_hcb_arrays in [crate::encoder_acpl3]. Same shape
      applied to BETA3 F0 (cb_off = 4 Coarse / 8 Fine — dequantize_beta3
      multiplies the signed lane by beta3_delta directly). BETA F0 stays
      at cb_off = 0 (unsigned magnitude — dequantize_beta_index takes
      unsigned_abs and re-applies the sign from the differential
      accumulator). Companion comment edits document the asymmetry.
    • 3 new round-174 unit tests:
      [alpha_f0_signed_lanes_round_trip_fine_and_coarse] sweeps every
      signed lane in both ALPHA F0 codebooks through encode → decode →
      decode value; [beta3_f0_signed_lanes_round_trip_fine_and_coarse]
      does the same for BETA3 F0; [alpha_f0_zero_alpha_picks_one_bit_peak]
      confirms the writer now picks the 1-bit symmetric peak for
      alpha_q = 0 (down from the pre-fix 10 / 12 bits). Total tests
      776 (was 773).
    • Round-128 family tests
      (encode_5_0_acpl1_real_alpha_emits_nonzero_alpha_when_surround_differs
      • ..._symmetric_scaling_yields_matching_alpha) re-shaped to assert
        on encoder byte-stream divergence rather than the decoder's
        recovered alpha_q — bit-position drift through the full 5_X
        SIMPLE/ASPX_ACPL_1 walker on non-silence input is independent of
        the F0 cb_off bug and still pending separate investigation (the
        user's "alpha_q desync" followup tracks it).

Added

  • Round 144 — 5_X SIMPLE/ASPX_ACPL_2 encoder with real per-parameter-
    band α + β extraction
    per ETSI TS 103 190-1 §4.2.6.6 Table 25 row
    case ASPX_ACPL_2: + §5.7.7.5 Pseudocode 116 + §5.7.7.6.1 Pseudocode
    117. The ACPL_2 counterpart to the round-132 5_X ACPL_1 real α+β path.

    • New Ac4ImsEncoder::encode_frame_pcm_5_0_acpl2_real_alpha_beta
      (+ ..._with_max_sfb) accepts a 5-channel [L, R, C, Ls, Rs] input
      and produces a 5_X ASPX_ACPL_2 frame whose two trailing
      acpl_data_1ch() elements carry per-parameter-band α + β indices
      extracted from the (L, Ls) and (R, Rs) MDCT energy ratios.
    • The on-wire body layout is the round-100
      build_5_x_acpl2_body_from_pcm_spectra layout (no joint-MDCT
      residual layer — ACPL_2 reconstructs the surround from L/R + the two
      acpl_data_1ch() parameter sets at decode time); the Ls/Rs spectra
      are consumed only by the α + β extractors and are not transmitted.
    • New encoder_acpl3::build_5_x_acpl2_body_from_pcm_spectra_real_alpha_beta
      builder reuses the round-128 / 132 shared α + β analytic primitives
      (compute_per_band_correlations / analytic_alpha_per_band /
      compute_per_band_energies / analytic_beta_per_band /
      quantise_alpha / quantise_beta_magnitude) and the
      write_acpl_data_1ch_real_alpha_beta writer with start_band = 0
      (acpl_config_1ch(FULL) carries no qmf_band) so every parameter band
      participates in the α + β coding.
    • β analytic derivation per Pseudocode 116 with yx0 and
      E[y²] ≈ E[x0²]: E[Ls²] = 0.5 · E[L²] · ((1 − α)² + β²)
      β = √max(0, 2·E[Ls²]/E[L²] − (1 − α_dq)²).
    • Total tests 773 (was 766): 7 new round-144 tests covering 5-channel
      AudioFrame round-trip, decoder mode resolution, on-wire body
      divergence from the round-100 scaffold for non-trivial surround,
      direct extract_beta_q_per_band non-zero gate, silence round-trip,
      encoder determinism, and structural pair0/pair1 population.
    • Deferred: real β extraction for ACPL_3 paths; real ASPX envelope
      coding; the round-128 ALPHA F0 writer-side alpha_q desync
      (deferred since r132) which currently obscures per-band on-wire α/β
      recovery through the full PCM→MDCT→writer→parser→synth chain when
      the analytic α quantises to a non-center lane.
  • Round 139 — 7.1-with-LFE (3/4/0.1) SIMPLE/ASPX_ACPL_1 encoder
    with real per-parameter-band α + β extraction
    per ETSI TS 103 190-1
    §4.2.6.14 Table 33 row case ASPX_ACPL_1: with b_has_lfe = 1 +
    §5.7.7.5 Pseudocode 116 + §5.7.7.6.1 Pseudocode 117. The LFE
    counterpart of the round-135 7.0 immersive real-α+β path.

    • New Ac4ImsEncoder::encode_frame_pcm_7_1_acpl1_real_alpha_beta
      (+ ..._with_max_sfb) reuses the round-135
      encoder_acpl3::build_7_x_acpl1_body_from_pcm_spectra_real_alpha_beta
      builder with the LFE coeffs_lfe + max_sfb_lfe slots populated,
      emitting a leading mono_data(b_lfe = 1) element (Table 21 +
      sf_info_lfe() Table 35) between the I-frame config block and
      companding_control(5) — exactly where the decoder's
      parse_7x_audio_data_outer(b_has_lfe = true) reads
      if (b_has_lfe) mono_data(1);.
    • The on-wire body structure matches the existing round-118 7.1
      ACPL_1 path. Decoder resolves SevenXCodecMode::AspxAcpl1 with
      b_has_lfe = true, both acpl_data_1ch_pair[0/1] populated (now
      carrying real α + β), joint-MDCT residual layer walked, LFE
      IMDCT'd into slot 7. A 60 Hz LFE tone round-trips to a non-silent
      reconstructed LFE channel.
    • +6 tests (total 766, was 760).
  • Round 132 — 5.0 SIMPLE/ASPX_ACPL_1 encoder with real per-parameter-
    band β extraction
    per ETSI TS 103 190-1 §5.7.7.5 Pseudocode 116 +
    §5.7.7.6.1 Pseudocode 117. Extends the round-128 real-α path: β was
    pinned to 0 (pure level-only surround image); round 132 derives a real
    per-band β magnitude from the surround/carrier energy residual that
    remains after α removes the level component.

    • Per Pseudocode 116 with the decorrelator output yx0 and
      E[y²] ≈ E[x0²]: E[Ls²] = 0.5·E[x0²]·((1-α)² + β²). Solving for
      the magnitude gives β = √max(0, 2·E[Ls²]/E[x0²] − (1-α)²), where
      α is the dequantised value the decoder reconstructs (so β closes
      the balance against the actual (1 − α_dq)).
    • New encoder_acpl3 helpers: compute_per_band_energies (per-band
      Σ x² for carrier + surround), analytic_beta_per_band (the
      energy-residual β estimator), quantise_beta_magnitude (nearest
      beta_q index against the Table 204 / 206 column-0 grid),
      write_acpl_beta_f0_value / write_acpl_beta_df_value (ACPL BETA
      F0 + DF codebook emitters, Tables A.40 / A.41), and the optional-β
      write_acpl_data_1ch_real_alpha_beta body writer.
    • New build function
      build_5_x_acpl1_body_from_pcm_spectra_real_alpha_beta + encoder
      entry points Ac4ImsEncoder::encode_frame_pcm_5_0_acpl1_real_alpha_beta
      (+ ..._with_max_sfb). The on-wire body structure is unchanged from
      the round-128 path — the decoder resolves FiveXCodecMode::AspxAcpl1,
      both acpl_data_1ch_pair[0/1] populated, and the β layer now carries
      real magnitudes.
    • Public extractor/validator entry points extract_alpha_q_per_band,
      extract_beta_q_per_band, and write_acpl_data_1ch_real_alpha_beta_bytes
      for round-trip testing.
    • β / β3 / γ otherwise stay at the round-95 / 100 / 103 / 128 scaffold
      for non-ACPL_1 paths. Total tests 755 (was 743).
    • Followup (round 128 latent bug, not introduced here): the ACPL
      ALPHA F0/DF writer (write_acpl_alpha_f0_value/_df_value) clamps a
      negative alpha_q to lane 0, writing the wrong codeword and
      desyncing the rest of the acpl_data_1ch element. It only round-trips
      correctly for alpha_q ≥ 0. The round-132 β coding contract is
      verified byte-exact via the isolated acpl_data_1ch round-trip test;
      the full-substream PCM path inherits the round-128 α-writer's
      in-range limitation. Both the α-writer sign/offset fix and real β
      extraction for the 7_X / ACPL_2 / ACPL_3 paths remain deferred.
  • Round 128 — 5.0 SIMPLE/ASPX_ACPL_1 encoder with real per-parameter-
    band α extraction
    per ETSI TS 103 190-1 §5.7.7.5 Pseudocode 116 +
    §5.7.7.6.1 Pseudocode 117. Replaces the round-103 zero-delta scaffold
    for the α coefficient family in the ACPL_1 path (β / β3 / γ stay at
    the round-95 / 100 / 103 zero-delta scaffold — β3 / γ only fire in
    ASPX_ACPL_3 anyway).

    • Per Pseudocode 116, above acpl_qmf_band: z0 = 0.5·(x0·(1+α) + y·β), z1 = 0.5·(x0·(1-α) - y·β) (then z1 *= √2 per
      Pseudocode 117). With β = 0 the surround reconstruction is a pure
      level-only image: Ls_recon = 0.5/√2 · L · (1 − α). Solving for α
      that minimises (Ls − 0.5/√2·L·(1−α))² per parameter band gives
      α = 1 − 2·√2 · ⟨L, Ls⟩ / ⟨L, L⟩.
    • New encoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra_real_alpha
      — mirrors build_5_x_acpl1_body_from_pcm_spectra (round 103) but
      with real α emitted via the ACPL ALPHA F0 + DF codebooks (Tables
      A.35 / A.34). Helper functions:
      • mdct_bin_to_param_band — maps MDCT bin → QMF subband sb = bin · 64 / transform_length → parameter band via
        acpl::sb_to_pb (§5.7.7.2 Table 197).
      • compute_per_band_correlations — computes (Σ x·y, Σ x²) per
        parameter band over the MDCT carrier vs. surround spectra,
        skipping bands below start_pb (the PARTIAL acpl_qmf_band
        maps to a start_pb > 0; bands below are M/S-recovered by the
        synth and α has no effect there).
      • analytic_alpha_per_band — closed-form α with clamp to ±2.0.
      • quantise_alpha — nearest-neighbour to ALPHA_DQ_FINE (Table
        203) / ALPHA_DQ_COARSE (Table 205); returns the signed
        alpha_q in -N/2..=+N/2.
      • write_acpl_alpha_f0_value / write_acpl_alpha_df_value
        emit ALPHA F0 (first band) + ALPHA DF (subsequent bands using
        delta_q = alpha_q[pb] - alpha_q[pb-1]) codewords per the
        acpl_hcb_arrays table family.
      • write_acpl_data_1ch_real_alpha — full acpl_data_1ch() body
        with real α + zero-delta β (β / β3 / γ fall back to
        write_acpl_*_zero from round 95).
    • New encoder entry points:
      • Ac4ImsEncoder::encode_frame_pcm_5_0_acpl1_real_alpha(&[L, R, C, Ls, Rs])
      • Ac4ImsEncoder::encode_frame_pcm_5_0_acpl1_real_alpha_with_max_sfb(.., max_sfb, max_sfb_master)
    • The on-wire body structure is identical to the round-103 path: the
      decoder resolves FiveXCodecMode::AspxAcpl1, parses both
      acpl_data_1ch_pair[0/1] slots, walks the joint-MDCT residual
      layer, and synthesises [L, R, C, Ls, Rs] via
      acpl_synth::run_acpl_5x_pair_pcm. The only difference is that
      the α huffman values now carry per-band non-zero deltas chosen by
      the encoder rather than the structural zero scaffold.
    • 6 new integration tests in
      tests/round128_5_x_acpl1_real_alpha.rs: end-to-end round-trip,
      decoder mode resolution, non-zero α emission when surround
      differs from carrier, silence round-trip, symmetric scaling
      yields a positive α in both pairs, encoder determinism.
    • Total test count: 743 (was 737) — 0 ignored, 0 failed.
    • Follow-ups (deferred): real per-band β / β3 / γ extraction
      (β = 0 simplification is spec-defensible for "level-only"
      encoding); real ASPX envelope coding; real Table-181 SAP-derived
      residual content; same real-α uplift for the ACPL_2 / ACPL_3 5_X
      paths and the 7_X ACPL_1 / ACPL_2 paths; back-pair Lb/Rb
      carriage on the ACPL paths; DT-mode (DIFF_TIME) coding using
      cross-frame state.
  • Round 125 — 7.0 (3/4/0) SIMPLE/Cfg3Five multichannel encoder
    path
    per ETSI TS 103 190-1 §4.2.6.14 Table 33 + §4.2.7.5 Table 29
    (five_channel_data()) + §4.2.7.4 Table 26 (additional-channel
    two_channel_data()). The non-LFE immersive counterpart of
    round-91's 7.1 SIMPLE encoder (the 7_X analogue of round 74's 5.0 vs
    round 80's 5.1).

    • Ac4ImsEncoder::with_7_0() — flips the TOC channel_mode prefix to
      0b1111000 (7 b — Table 85 channel_mode 5, 7.0 (3/4/0) → 7
      channels). The decoder's walk_ac4_substream then dispatches
      channels == 7 through parse_7x_audio_data_outer(b_has_lfe = false).
    • Ac4ImsEncoder::encode_frame_pcm_7_0(&[L, R, C, Ls, Rs, Lb, Rb])
      • ..._with_max_sfb(&[..], max_sfb, max_sfb_add) — emit IMS v2
        frames in 7_X_codec_mode = SIMPLE (0) + coding_config = Cfg3Five (3). The five front/surround channels share the
        Cfg3Five five_channel_data() body (the same shape as round-74
        5.0 / round-80 5.1 / round-91 7.1); the immersive back pair
        Lb/Rb rides a trailing identity-SAP two_channel_data()
        (b_use_sap_add_ch = 0, sap_mode = 0 on the shared
        chparam_info) so the decoder's
        dispatch_7x_additional_channel_pair routes Lb/Rb directly into
        output slots 5/6 (Table 183 row "3/4/0.x" identity path).
        max_sfb defaults to 40; max_sfb_add defaults to 40.
    • New encoder_asf::build_7_0_simple_asf_body_from_pcm_spectra
      emits the substream body bytes. Body layout: 7_X_codec_mode = SIMPLE (0) (2 b) + coding_config = 3 (2 b) +
      five_channel_data() (shared sf_info + 5x sf_data per Table 29)
      • b_use_sap_add_ch = 0 (1 b) + two_channel_data() (Lb/Rb per
        Table 26). The body is structurally the round-91 7.1 body with
        the leading mono_data(b_lfe = 1) element omitted (the walker's
        if (b_has_lfe) mono_data(1); branch is gated off for
        channel_mode 5). No companding (SIMPLE), no ASPX trailers
        (SIMPLE), no ACPL pair (SIMPLE), no trailing mono_data(0)
        (Cfg3Five — the 7.X trailing-mono gate is coding_config in {0, 2} only).
    • Decoder round-trip verified: 7.0 → 7-channel S16 interleaved PCM
      (1920 × 7 × 2). The 7.0 walker resolves seven_x_mode == Simple,
      seven_x_b_has_lfe == false, lfe_mono_data == None,
      five_channel_data populated with five non-empty
      scaled_spec_per_channel entries, identity-SAP
      seven_x_additional_channel_data populated with two non-empty
      scaled_spec_per_channel entries for the Lb/Rb pair. Slots 0..4
      synthesise via dispatch_5x_cfg3_simple_aspx (round 39); slots
      5/6 via dispatch_7x_additional_channel_pair (round 39/40
      identity-SAP path).
    • Per-channel spectral SNR on the 220/440/660/880/1100/1320/1540 Hz
      independent-tone 7.0 fixture: L=24.5 / R=24.8 / C=25.0 / Ls=23.4 /
      Rs=27.4 / Lb=25.4 / Rb=26.0 dB — all above the ≥ 20 dB floor,
      matching the round-74 / 80 / 91 SNR numbers exactly (the encoder
      reuses the same per-channel forward pipeline).
    • 8 new integration tests in tests/round125_7_0_multichannel.rs
      (7-channel layout, TOC declares 7 channels, sequence-counter roll,
      with_7_0() builder smoke-test, substream-walker confirms
      b_has_lfe = false + no LFE + additional pair populated,
      independent-tones round-trip, silence round-trip, per-channel
      spectral SNR ≥ 20 dB). The test helper wraps the encoder's
      raw_ac4_frame() payload in an Annex G 0xAC40 + frame_size
      sync header so the decoder's find_sync_frame latches onto the
      genuine sync word rather than an incidental 0xAC40 byte pair in
      the body (a hazard round 91's fixture happened to dodge by data
      luck).
    • Total test count: 737 (was 729) — 0 ignored, 0 failed.
    • Follow-ups (deferred, unchanged from round 118): real per-band
      (alpha, beta) extraction replacing the zero-delta scaffold;
      real ASPX envelope coding; real Table-181 SAP-derived residual
      content; back-pair Lb/Rb carriage on the ACPL paths (currently
      silent on the 7_X ACPL_1 / ACPL_2 paths since those modes carry
      no SIMPLE/ASPX additional-channel block).
  • Round 118 — 7.0 / 7.1 (3/4/0(.1)) SIMPLE/ASPX_ACPL_1 multichannel
    encoder path
    per ETSI TS 103 190-1 §4.2.6.14 Table 33 row
    case ASPX_ACPL_1: (+ b_has_lfe = 1 for 7.1 — §4.2.6.5 Table 21
    mono_data(b_lfe) + §4.2.8 Table 35 sf_info_lfe()). The 7_X
    (immersive) counterpart to the round-103 5_X ASPX_ACPL_1 encoder and the
    encoder side of the decoder's round-27 parse_7x_audio_data_outer
    ASPX_ACPL_1 branch (which already reads the joint-MDCT residual layer).
    Closes the first deferred follow-up from round 114.

    • Ac4ImsEncoder::encode_frame_pcm_7_0_acpl1(&[L, R, C, Ls, Rs, Lb, Rb])
      • ..._with_max_sfb(&[..], max_sfb, max_sfb_master) — emit IMS v2
        frames in 7_X_codec_mode = ASPX_ACPL_1 (2). Channel_mode prefix
        forced to 0b1111000 (7 b — Table 85 channel_mode 5, 7.0 (3/4/0)) so
        the decoder dispatches channels == 7 through
        parse_7x_audio_data_outer(b_has_lfe = false). max_sfb defaults to
        40; max_sfb_master (residual band bound) defaults to 20.
    • Ac4ImsEncoder::encode_frame_pcm_7_1_acpl1(&[L, R, C, Ls, Rs, Lb, Rb, LFE]) + ..._with_max_sfb(&[..], max_sfb, max_sfb_master, max_sfb_lfe)
      — the LFE counterpart: identical body plus a leading
      mono_data(b_lfe = 1) between the I-frame config block and
      companding_control(5), exactly where
      parse_7x_audio_data_outer(b_has_lfe = true) reads
      if (b_has_lfe) mono_data(1);. Channel_mode prefix forced to
      0b1111001 (7 b — Table 88 channel_mode 6, 7.1) → channels == 8.
      max_sfb_lfe defaults to 7 (LFE-spec cap at tl = 1920,
      n_msfbl_bits = 3).
    • New encoder_acpl3::build_7_x_acpl1_body_from_pcm_spectra — the 7_X
      ASPX_ACPL_2 body (round 107/114) with three structural differences,
      the same three that separate the 5_X ACPL_1 path from the 5_X ACPL_2
      path: (1) 7_X_codec_mode = 2 (vs 3); (2) acpl_config_1ch is
      PARTIAL via the shared write_acpl_config_1ch_partial emitter (6 b —
      carries acpl_qmf_band_minus1, so acpl_data_1ch() start_band
      resolves from qmf_band via sb_to_pb); (3) an explicit joint-MDCT
      residual layer via the shared write_acpl_1_residual_layer emitter
      (max_sfb_master + 2× chparam_info + 2× sf_data(ASF)) carrying the
      Ls/Rs surround pair (sSMP,3 / sSMP,4 per Table 181) after the two
      two_channel_data() pairs and before the trailing Cfg0 centre
      mono_data(0). The SIMPLE/ASPX additional-channel block is skipped
      (the decoder only walks it for SIMPLE/Aspx modes). Reuses the
      round-80 write_lfe_mono_data emitter for the 7.1 LFE element.
    • Decoder round-trip verified: 7.0 ACPL_1 → 7-channel S16 interleaved
      PCM (1920 × 7 × 2); 7.1 ACPL_1 → 8-channel S16 (with LFE slot 7). The
      decoder resolves seven_x_mode == AspxAcpl1, the PARTIAL config
      (non-zero qmf_band), both two_channel_data pairs, the residual
      pair + max_sfb_master, the Cfg0 centre, and both
      acpl_data_1ch_pair[0/1]. The LFE spectrum IMDCT's into slot 7 (round
      80 render); [L, R, C, Ls, Rs] slots 0..4 synthesise via
      acpl_synth::run_acpl_5x_pair_pcm; the back pair Lb/Rb (slots 5/6)
      stays silent per the Table 202 mapping.
    • 8 new integration tests in tests/round118_7_x_acpl1_encoder.rs
      (7-channel + 8-channel layout, sequence-counter roll, 7.0 + 7.1
      full-body decoder resolution, LFE-slot-non-silent for a 60 Hz LFE
      tone, silence round-trip, small-residual-budget round-trip recovering
      the clamped max_sfb_master).
    • Total test count: 729 (was 721) — 0 ignored, 0 failed.
    • Follow-ups (deferred): real per-band (alpha, beta) extraction
      replacing the zero-delta scaffold; real ASPX envelope coding; real
      Table-181 SAP-derived residual content (the residual sf_data
      currently codes the raw Ls/Rs spectra); back-pair Lb/Rb carriage
      (currently silent on the ACPL paths).
  • Round 114 — 7.1 (3/4/0.1) SIMPLE/ASPX_ACPL_2 multichannel encoder
    path
    per ETSI TS 103 190-1 §4.2.6.14 Table 33 row case ASPX_ACPL_2:
    with b_has_lfe = 1 + §4.2.6.5 Table 21 (mono_data(b_lfe)) + §4.2.8
    Table 35 (sf_info_lfe()) + Table 106 column 4 (n_msfbl_bits). The
    LFE counterpart of the round-107 7.0 ASPX_ACPL_2 encoder — the body is
    identical except a leading mono_data(b_lfe = 1) element is emitted
    between the I-frame config block and companding_control(5), exactly
    where the decoder's parse_7x_audio_data_outer(b_has_lfe = true) reads
    if (b_has_lfe) mono_data(1); (§4.2.6.14 Table 33). Closes the first
    deferred follow-up from round 107.

    • Ac4ImsEncoder::encode_frame_pcm_7_1_acpl2(&[L, R, C, Ls, Rs, Lb, Rb, LFE]) + ..._with_max_sfb(&[..], max_sfb, max_sfb_lfe) — emit IMS v2
      frames in 7_X_codec_mode = ASPX_ACPL_2 (3) with the LFE element.
      Channel_mode prefix forced to 0b1111001 (7 b — Table 88 channel_mode
      6, 7.1 (3/4/0.1)) so the decoder dispatches channels == 8 through
      parse_7x_audio_data_outer(b_has_lfe = true). max_sfb defaults to
      40; max_sfb_lfe defaults to 7 (the LFE-spec cap at tl = 1920,
      n_msfbl_bits = 3).
    • encoder_acpl3::build_7_x_acpl2_body_from_pcm_spectra gained two
      parameters — max_sfb_lfe: Option<u32> and coeffs_lfe: Option<&[f32]>. When both are Some the shared
      write_lfe_mono_data emitter (round 80) prepends the LFE
      mono_data(b_lfe = 1) element at the spec-correct position; with both
      None the body is the unchanged round-107 7.0 form (the round-107
      7.0 caller passes None/None).
    • Decoder round-trip verified: 7.1 ACPL_2 → 8-channel S16 interleaved
      PCM (1920 samples × 8 ch × 2 bytes). The decoder walks the full
      Table 33 ASPX_ACPL_2 + LFE body and resolves seven_x_mode == AspxAcpl2, seven_x_b_has_lfe == true, lfe_mono_data.is_some(),
      acpl_config_1ch_full.is_some(), two_channel_data.len() == 2,
      cfg0_centre_mono.is_some(), and both acpl_data_1ch_pair[0/1]. The
      LFE spectrum is IMDCT'd into slot 7 via the existing round-80 LFE
      render (Ac4Decoder::receive_frame, channels == 8); the round-107
      [L, R, C, Ls, Rs] slots 0..4 synthesis (via
      acpl_synth::run_acpl_5x_pair_pcm) and the silent back pair Lb/Rb
      (slots 5/6, Table 202 ACPL_2 mapping) are unchanged.
    • 6 new integration tests in tests/round114_7_1_acpl2_encoder.rs
      (8-channel layout, sequence-counter roll, full-body + LFE decoder
      resolution, LFE-slot-non-silent for a 60 Hz LFE tone, silence
      round-trip, wide-max_sfb round-trip) + 1 new unit test in
      encoder_acpl3::tests (build_7_x_acpl2_body_with_lfe_decoder_resolves_lfe).
    • Total test count: 721 (was 714) — 0 ignored, 0 failed.
    • Follow-ups (deferred): the 7_X ASPX_ACPL_1 path (PARTIAL config +
      joint-MDCT residual layer, the 7_X analogue of round 103); real
      per-band (alpha, beta) extraction replacing the zero-delta scaffold;
      real ASPX envelope coding; back-pair Lb/Rb carriage (currently silent
      on the ACPL_2 path).
  • Round 107 — 7.0 SIMPLE/ASPX_ACPL_2 multichannel encoder path per
    ETSI TS 103 190-1 §4.2.6.14 Table 33 row case ASPX_ACPL_2: +
    §4.2.12.1 Table 50 (aspx_config) + §4.2.13.1 Table 59
    (acpl_config_1ch FULL) + §4.2.7.4 Table 26 (two_channel_data) +
    §4.2.6.5 Table 21 (mono_data) + §4.2.12.4 Table 52 (aspx_data_2ch) +
    §4.2.12.3 Table 51 (aspx_data_1ch) + §4.2.13.3 Table 61
    (acpl_data_1ch) + §4.2.11 Table 49 (companding_control). The 7_X
    (immersive) symmetric counterpart to the round-100 5_X ASPX_ACPL_2
    encoder and the encoder side of the decoder's round-27
    parse_7x_audio_data_outer ASPX_ACPL_2 branch. Reuses the same 1ch
    ACPL / ASPX parameter shape (Pseudocode 117) as the 5_X path but emits
    the 7_X channel element's distinct framing.

    • Ac4ImsEncoder::encode_frame_pcm_7_0_acpl2(&[L, R, C, Ls, Rs, Lb, Rb])
      • ..._with_max_sfb(&[..], max_sfb) — emit IMS v2 frames in
        7_X_codec_mode = ASPX_ACPL_2 (3). Channel_mode prefix forced to
        0b1111000 (7 b — Table 85 channel_mode 5, 7.0 (3/4/0)) so the
        decoder dispatches channels == 7 through
        parse_7x_audio_data_outer(b_has_lfe = false).
    • encoder_acpl3::build_7_x_acpl2_body_from_pcm_spectra — shared body
      builder. Layout: 7_X_codec_mode = 3 (2 b, vs the 5_X 3-bit
      field) + I-frame block (aspx_config() 15 b + acpl_config_1ch(FULL)
      3 b) + companding_control(5) (sync=1, on=1 — the 2-bit sync-on wire
      shape is identical to companding_control(2/3)) + coding_config = 0
      (2 b, Cfg0) + b_2ch_mode + two_channel_data() (L/R carriers) +
      two_channel_data() (Ls/Rs carriers) + trailing Cfg0 mono_data(0)
      (centre) + I-frame aspx_data_2ch() + aspx_data_2ch() + aspx_data_1ch() envelope trailer + two acpl_data_1ch() parameter
      sets (Pseudocode 117 D0 / D1). Structural differences from the 5_X
      ACPL_2 path: 2-bit codec_mode, 2-bit coding_config, the centre
      mono_data(0) moves out of the coding_config switch to a single
      trailing element, the body carries two stereo pairs (the surround pair
      rides the second two_channel_data), and the ASPX trailer carries an
      extra aspx_data_2ch(). The ASPX_ACPL_1-only joint-MDCT residual
      layer and the SIMPLE/ASPX additional-channel block are both skipped
      for ASPX_ACPL_2.
    • Decoder round-trip verified: 7.0 ACPL_2 → 7-channel S16 interleaved
      PCM (1920 samples × 7 ch × 2 bytes). The decoder walks the full
      Table 33 ASPX_ACPL_2 body and resolves
      seven_x_mode == AspxAcpl2, acpl_config_1ch_full.is_some(),
      two_channel_data.len() == 2, cfg0_centre_mono.is_some(), and both
      acpl_data_1ch_pair[0/1].is_some() — the existing round-37/40 7_X
      pair dispatch synthesises [L, R, C, Ls, Rs] (slots 0..4) via
      acpl_synth::run_acpl_5x_pair_pcm; the back pair Lb/Rb (slots 5/6)
      stays silent per the documented Table 202 ACPL_2 channel mapping.
    • 5 new integration tests in tests/round107_7_x_acpl2_encoder.rs
      (7-channel layout, sequence-counter roll, full-body decoder resolution
      including the two-pair Cfg0 + no-residual assertion, silence
      round-trip, wide-max_sfb round-trip) + 1 new unit test in
      encoder_acpl3::tests (full-body decoder resolution via
      parse_7x_audio_data_outer).
    • Total test count: 714 (was 708) — 0 ignored, 0 failed.
    • Follow-ups (deferred): the 7.1 (LFE) ASPX_ACPL_2 path
      (b_has_lfe = true leading mono_data(1)); the 7_X ASPX_ACPL_1 path
      (PARTIAL config + joint-MDCT residual layer, the 7_X analogue of
      round 103); real per-band (alpha, beta) extraction replacing the
      zero-delta scaffold; real ASPX envelope coding; back-pair Lb/Rb
      carriage (currently silent on the ACPL_2 path).
  • Round 103 — 5_X SIMPLE/ASPX_ACPL_1 multichannel encoder path per
    ETSI TS 103 190-1 §4.2.6.6 Table 25 row case ASPX_ACPL_1: +
    §4.2.12.1 Table 50 (aspx_config) + §4.2.13.1 Table 59
    (acpl_config_1ch PARTIAL) + §4.2.7.4 Table 26 (two_channel_data) +
    §4.2.10.1 Table 47 (chparam_info) + §4.2.6.5 Table 21 (mono_data) +
    §4.2.12.4 Table 52 (aspx_data_2ch) + §4.2.12.3 Table 51
    (aspx_data_1ch) + §4.2.13.3 Table 61 (acpl_data_1ch) + §4.2.11
    Table 49 (companding_control). Completes the round-100 follow-up: the
    symmetric encoder for the decoder's round-25
    parse_aspx_acpl_1_2_inner_body ASPX_ACPL_1 branch (Pseudocode 117),
    including the joint-MDCT residual layer that ASPX_ACPL_2 omits.
    Extends the encoder_acpl3 module:

    • Ac4ImsEncoder::encode_frame_pcm_5_0_acpl1(&[L, R, C, Ls, Rs]) +
      ..._with_max_sfb(&[L, R, C, Ls, Rs], max_sfb, max_sfb_master)
      emit IMS v2 frames in 5_X_codec_mode = ASPX_ACPL_1 (2).
      Channel_mode prefix forced to 0b1101 (5 ch) per Table 85 so the
      decoder dispatches channels == 5 through
      parse_5x_audio_data_outer(b_has_lfe = false).
    • encoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra — shared body
      builder. Layout: 5_X_codec_mode = 2 (3 b) + I-frame block
      (aspx_config() 15 b + acpl_config_1ch(PARTIAL) 6 b) +
      companding_control(3) (sync=1, on=1) + coding_config = 0 (1 b) +
      two_channel_data() (L/R carriers) + ASPX_ACPL_1 joint-MDCT
      residual layer
      (max_sfb_master in n_side bits + 2× chparam_info
      • sf_data(ASF) for the Ls/Rs surround pair sSMP,3 / sSMP,4 per
        Table 181) + Cfg0 mono_data(0) (centre) + I-frame aspx_data_2ch()
      • aspx_data_1ch() + two acpl_data_1ch() parameter sets
        (Pseudocode 117 D0 / D1). The residual layer + PARTIAL config are the
        two structural differences from the round-100 ACPL_2 path: ASPX_ACPL_1
        transmits the surround residual explicitly (so the encoder takes a
        full 5-channel input) rather than reconstructing Ls/Rs purely from the
        L/R carriers.
    • New bit-exact emitters: write_acpl_config_1ch_partial (Table 59,
      2-bit id + 1-bit quant_mode + 3-bit acpl_qmf_band_minus1) and
      write_acpl_1_residual_layer (max_sfb_master clamped to the n_side
      band budget + identity-SAP chparam pair + two sf_data(ASF) bodies
      via the shared prepare_stereo_channel forward pipeline). The
      acpl_data_1ch() start_band is resolved from the PARTIAL config's
      qmf_band via crate::acpl::sb_to_pb (vs ACPL_2's FULL config →
      start_band 0).
    • Decoder round-trip verified: 5.0 ACPL_1 → 5-channel S16 interleaved
      PCM (1920 samples × 5 ch × 2 bytes). The decoder walks the full
      Table 25 ASPX_ACPL_1 body and resolves five_x_mode == AspxAcpl1,
      acpl_config_1ch_partial.is_some() (with non-zero qmf_band),
      acpl_1_residual_max_sfb_master.is_some(), both
      acpl_1_residual_pair[0/1].is_some() (sSMP,3 / sSMP,4),
      cfg0_centre_mono.is_some(), and both acpl_data_1ch_pair[0/1]
      the 5-channel [L, R, C, Ls, Rs] synthesis runs via
      acpl_synth::run_acpl_5x_pair_pcm (Pseudocode 117).
    • 5 new integration tests in tests/round103_5_x_acpl1_encoder.rs
      (5-channel layout, sequence-counter roll, full-body decoder
      resolution including the residual layer, silence round-trip,
      small-residual-budget round-trip) + 3 new unit tests in
      encoder_acpl3::tests (acpl_config_1ch_partial round-trip,
      residual-layer clamp behaviour, full-body decoder resolution).
    • Total test count: 708 (was 700) — 0 ignored, 0 failed.
    • Follow-ups (deferred): real per-band (alpha, beta) parameter
      extraction replacing the zero-delta scaffold; real ASPX envelope
      coding; real joint-MDCT residual content (the residual sf_data
      currently codes the raw Ls/Rs spectra — a future round should derive
      the proper sSMP,3 / sSMP,4 residual from the Table-181 SAP first
      stage); matching 7_X ASPX_ACPL_{1,2} encoder paths (the 7_X walker
      shares this 1ch acpl/aspx shape).
  • Round 100 — 5_X SIMPLE/ASPX_ACPL_2 multichannel encoder path per
    ETSI TS 103 190-1 §4.2.6.6 Table 25 row case ASPX_ACPL_2: +
    §4.2.12.1 Table 50 (aspx_config) + §4.2.13.1 Table 59
    (acpl_config_1ch FULL) + §4.2.7.4 Table 26 (two_channel_data) +
    §4.2.6.5 Table 21 (mono_data) + §4.2.12.4 Table 52 (aspx_data_2ch)

    • §4.2.12.3 Table 51 (aspx_data_1ch) + §4.2.13.3 Table 61
      (acpl_data_1ch) + §4.2.11 Table 49 (companding_control). Symmetric
      counterpart to the decoder's round-25 parse_aspx_acpl_1_2_inner_body
      walker (Pseudocode 117). Extends the encoder_acpl3 module:
    • Ac4ImsEncoder::encode_frame_pcm_5_0_acpl2(&[L, R, C]) +
      ..._with_max_sfb(&[L, R, C], max_sfb) — emit IMS v2 frames in
      5_X_codec_mode = ASPX_ACPL_2 (3). Channel_mode prefix forced to
      0b1101 (5 ch) per Table 85 so the decoder dispatches channels == 5 through parse_5x_audio_data_outer(b_has_lfe = false).
    • encoder_acpl3::build_5_x_acpl2_body_from_pcm_spectra — shared
      body builder. Layout: 5_X_codec_mode = 3 (3 b) + I-frame block
      (aspx_config() 15 b + acpl_config_1ch(FULL) 3 b) +
      companding_control(3) (sync=1, on=1) + coding_config = 0 (1 b,
      AcplLite2 / two-channel false-branch) + two_channel_data() (L/R
      carriers) + Cfg0 mono_data(0) (centre carrier) + I-frame
      aspx_data_2ch() + aspx_data_1ch() + two acpl_data_1ch()
      parameter sets (Pseudocode 117 D0 / D1). The ASPX_ACPL_1-only
      joint-MDCT residual layer (max_sfb_master + 2× chparam_info + 2× sf_data) is skipped for ACPL_2 — that's the structural
      difference that makes the ACPL_2 path the cleanest encoder win.
    • New bit-exact emitters: write_acpl_config_1ch_full (Table 59,
      2-bit id + 1-bit quant_mode, no qmf_band), write_two_channel_data
      (Table 26 shared sf_info(ASF) + identity-SAP chparam_info +
      sf_data), write_mono_data_centre (Table 21 non-LFE:
      spec_frontend = 0 + sf_info(ASF) + sf_data),
      write_aspx_data_1ch_minimal (Table 51 FIXFIX num_env=1 path),
      write_acpl_data_1ch_minimal (Table 61: acpl_framing_data +
      acpl_ec_data(ALPHA) + acpl_ec_data(BETA), 1 param set, DF
      zero-delta). The 1ch ASPX SIGNAL band count uses
      num_sbg_sig_highres (matching parse_aspx_ec_data's empty-
      freq_res fallback when freq_res_mode != Signalled).
    • Decoder round-trip verified: 5.0 ACPL_2 → 5-channel S16
      interleaved PCM (1920 samples × 5 ch × 2 bytes). The decoder
      walks the full Table 25 ASPX_ACPL_2 body and resolves
      five_x_mode == AspxAcpl2, acpl_config_1ch_full.is_some(),
      two_channel_data.len() == 1, cfg0_centre_mono.is_some(), and
      both acpl_data_1ch_pair[0/1].is_some() — the 5-channel
      [L, R, C, Ls, Rs] synthesis runs via
      acpl_synth::run_acpl_5x_pair_pcm (Pseudocode 117). With all-zero
      ACPL parameter deltas Ls/Rs collapses to ducker-driven
      reconstruction from the L/R carriers.
    • 4 new integration tests in tests/round100_5_x_acpl2_encoder.rs
      (5-channel layout, sequence-counter roll, full-body decoder
      resolution, silence round-trip) + 5 new unit tests in
      encoder_acpl3::tests (bit-order round-trips for
      acpl_config_1ch_full / two_channel_data / mono_data(0) /
      acpl_data_1ch + aspx_data_1ch emit).
    • Total test count: 700 (was 691) — 0 ignored, 0 failed.
    • Follow-ups (deferred): the ASPX_ACPL_1 encoder path (adds the
      joint-MDCT residual layer + PARTIAL-mode acpl_config_1ch with
      acpl_qmf_band); real per-band (alpha, beta) extraction
      replacing the zero-delta scaffold; matching 7_X ASPX_ACPL_{1,2}
      encoder paths (the 7_X walker shares this 1ch acpl/aspx shape).
  • Round 95 — 5_X SIMPLE/ASPX_ACPL_3 multichannel encoder path per
    ETSI TS 103 190-1 §4.2.6.6 Table 25 row case ASPX_ACPL_3: +
    §4.2.12.1 Table 50 (aspx_config) + §4.2.13.2 Table 60 (acpl_config_2ch)

    • §4.2.13.4 Table 62 (acpl_data_2ch) + §4.2.12.4 Table 52
      (aspx_data_2ch) + §4.2.11 Table 49 (companding_control) + §4.2.6.3
      Table 22 (stereo_data). Symmetric counterpart to the decoder's round-34
      parse_5x_audio_data_outer ASPX_ACPL_3 walker (5a58f6a). New
      encoder_acpl3 module:
    • Ac4ImsEncoder::encode_frame_pcm_5_0_acpl3(&[L, R, C]) and
      encode_frame_pcm_5_1_acpl3(&[L, R, C, LFE]) — emit IMS v2 frames
      in 5_X_codec_mode = ASPX_ACPL_3 (4). Channel_mode prefix forced to
      0b1101 (5 ch) / 0b1110 (6 ch) per Table 85.
    • encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra — shared
      body builder. Layout: 5_X_codec_mode = 4 (3 b) + I-frame block
      (aspx_config() 15 b + acpl_config_2ch() 4 b) + optional LFE
      mono_data(b_lfe = 1) + companding_control(2) (sync=1, on=1) +
      stereo_data() (split-MDCT path) + aspx_data_2ch() + acpl_data_2ch().
    • encoder_acpl3::write_aspx_config / write_acpl_config_2ch /
      write_companding_control_2ch_sync_on — bit-exact emitters for
      the small configuration elements. Round-trip-verified against the
      matching parsers via unit tests.
    • ASPX/A-CPL parameter bits emitted as minimum-bit-cost zero-delta
      Huffman codewords: pick_zero_delta_cw(len, cw, cb_off) picks the
      entry at index == cb_off (zero delta for DF/DT) and
      pick_min_len_cw picks the smallest-length entry (used for F0
      seeds). Covers all 18 ASPX HCBs (Annex A.2 Tables A.16-A.33) and
      all 24 ACPL HCBs (Annex A.3 Tables A.34-A.57).
    • write_aspx_data_2ch_minimal emits the FIXFIX + num_env=1 path
      with aspx_balance = 1, all-FREQ delta directions, and per-channel
      SBG counts derived from aspx::derive_aspx_frequency_tables.
    • write_acpl_data_2ch_minimal emits acpl_framing_data() (smooth
      interp, num_param_sets = 1) + 11 × acpl_huff_data() (alpha1/2,
      beta1/2/3, gamma1..6) with diff_type = 0 (FREQ) and zero-delta
      DF codewords.
    • Decoder round-trip verified: 5.0 ACPL_3 → 5-channel S16
      interleaved PCM (1920 samples × 5 ch × 2 bytes); 5.1 ACPL_3 →
      6-channel S16. The decoder walks the full Table 25 body
      (parse_stereo_data_body + parse_aspx_data_2ch_body +
      parse_acpl_data_2ch) and resolves five_x_mode == AspxAcpl3 +
      acpl_config_2ch.is_some() && acpl_data_2ch.is_some(). The 5-channel
      [L, R, C, Ls, Rs] synthesis runs via acpl_synth::run_acpl_5x_mch_pcm
      (Pseudocode 118) with all-zero ACPL parameter deltas — Ls/Rs
      collapses to ducker-driven reconstruction from the L/R carriers.
    • 4 new integration tests in tests/round95_5_x_acpl3_encoder.rs:
      encode_5_0_acpl3_produces_5_channel_audio_frame,
      encode_5_1_acpl3_produces_6_channel_audio_frame,
      encode_5_0_acpl3_advances_sequence_counter,
      encode_5_0_acpl3_decoder_resolves_aspx_acpl_3_mode.
    • 7 new unit tests in encoder_acpl3::tests covering bit-order
      round-trips for aspx_config / acpl_config_2ch / companding_control /
      acpl_data_2ch + minimum-cost / zero-delta HCB picker invariants.
    • Total test count: 691 (was 680) — 0 ignored, 0 failed.
    • Follow-ups (deferred to subsequent rounds): replace zero-delta
      ACPL parameter writer with real QMF-domain (alpha, beta, gamma)
      parameter extractor (per-band downmix correction estimated from
      L/R/Ls/Rs source PCM); replace zero-delta ASPX envelope coder
      with real envelope extraction; matching encoder paths for
      5_X_codec_mode in {ASPX_ACPL_1, ASPX_ACPL_2} (Pseudocode 117).
  • Round 91 — 7.1 (3/4/0.1) SIMPLE/ASF Cfg3Five multichannel forward
    analysis (7 SCE + LFE) encoder + decoder 7_X SIMPLE/Cfg3Five core
    render
    per ETSI TS 103 190-1 §4.2.6.14 Table 33 + §4.2.7.5 Table 29

    • §4.2.7.4 Table 26 + §4.2.8 Table 35 + §4.3.3.7.1 Table 88
      (channel_mode 6 = 7.1 (3/4/0.1)):
    • encoder_asf::build_7_1_simple_asf_body_from_pcm_spectra(transform_length, max_sfb, max_sfb_add, max_sfb_lfe, &[&[f32]; 8], pad_target_bytes)
      emits the full 7.1 multichannel audio_data body for 7_X_codec_mode = SIMPLE, b_has_lfe = 1, coding_config = 3 (Cfg3Five). Differs
      from the round-80 5.1 builder in three places per Table 33: (1)
      7_X_codec_mode is 2 bits (vs 3 for 5_X_codec_mode per Table 25);
      (2) SIMPLE skips the leading companding_control(5) (5_X SIMPLE
      skips it too, but the 7_X-only ASPX path also skips it whereas 5_X
      ASPX would emit it); (3) after the inner five_channel_data() for
      L/R/C/Ls/Rs the SIMPLE/ASPX additional-channel block emits
      b_use_sap_add_ch (1 b) = 0 + two_channel_data() carrying the
      immersive pair Lb/Rb (identity SAP with sap_mode = 0 on its
      chparam_info) per Table 26. No trailing mono_data(0) for Cfg3
      (gated on coding_config in {0, 2} only); no ASPX trailers / ACPL
      data pair for SIMPLE.
    • Ac4ImsEncoder::with_7_1() — channel-mode prefix 0b1111001 (7 b
      — Table 88 channel_mode 6) for the 7.1 (3/4/0.1) layout. Builder
      method parity with with_5_0() / with_5_1().
    • Ac4ImsEncoder::encode_frame_pcm_7_1(&[L, R, C, Ls, Rs, Lb, Rb, LFE])
      and encode_frame_pcm_7_1_with_max_sfb(..., max_sfb, max_sfb_add, max_sfb_lfe) — force the 7.1 channel_mode prefix so the decoder's
      walk_ac4_substream dispatches channels == 8 through
      parse_7x_audio_data_outer(b_has_lfe = true), run the round-50
      forward MDCT pipeline (KBD-windowed MDCT + DP-optimal sectioning +
      HCB1..11 + SNF) independently per channel, and wrap the body in
      the v2 IMS TOC.
    • Decoder 7_X SIMPLE/Cfg3Five core render in
      Ac4Decoder::receive_frame: when seven_x_mode in {SIMPLE, ASPX}
      and seven_x_coding_config == Cfg3Five, drive
      dispatch_5x_cfg3_simple_aspx on the inner five_channel_data to
      IMDCT slots 0..4 (L/R/C/Ls/Rs). The 7_X walker had been populating
      tools.five_channel_data for ~50+ rounds but the inner 5-channel
      PCM was never rendered — only slots 5/6 (the additional-pair F/G)
      and slot 7 (LFE, round 80) were touched. This change inherits the
      5_X core IMDCT/KBD/overlap-add chain unchanged (the per-channel
      body shape is identical to the 5_X Cfg3Five case). ASPX trailer
      plumbing (cfg3_aspx_lr / cfg3_aspx_ls_rs / cfg3_aspx_centre) is
      passed as None — the 7_X walker has its own ASPX trailer slots
      that need separate wiring (deferred).
    • Spectral SNR on the 220 / 440 / 660 / 880 / 1100 / 1320 /
      1540 Hz independent-tone 7.1 fixture: L=24.5 / R=24.8 / C=25.0 /
      Ls=23.4 / Rs=27.4 / Lb=25.4 / Rb=26.0 dB — all above the ≥ 20 dB
      floor, identical to round-80 5.1 for the L/R/C/Ls/Rs channels (same
      forward pipeline) with Lb/Rb tracking the same SNR-bandwidth
      relationship. The 60 Hz LFE tone round-trips to a non-silent
      reconstructed LFE channel via the shared round-80 LFE render in
      receive_frame.
    • New test suite tests/round91_7_1_multichannel.rs (8 tests):
      layout (8-channel S16 interleaved PCM), TOC channels=8, sequence
      counter roll, with_7_1() builder TOC contract, walker contract
      (b_has_lfe == true, populated lfe_mono_data.scaled_spec,
      populated five_channel_data + seven_x_additional_channel_data
      with b_use_sap_add_ch = false), silence round-trip, independent-
      tone round-trip with audible PCM in every non-LFE slot + audible
      LFE + verified Lb/Rb separation, and per-channel SNR ≥ 20 dB.
    • ACPL_3 multichannel ASPX / A-CPL encoder remains deferred (the
      5_X ASPX_ACPL_3 / 5_X ASPX_ACPL_{1,2} encoder paths haven't
      landed; 7_X ASPX modes are gated on those).
  • **Round 80 — 5.1 SIMPLE/ASF Cfg3Five multichannel forward analysis (5 SCE

    • LFE) encoder** per ETSI TS 103 190-1 §4.2.6.6 Table 25 (if (b_has_lfe) mono_data(1);) + §4.2.7.5 Table 29 + §4.2.8 (sf_info_lfe() Table 35 /
      Table 106 column 4 n_msfbl_bits):
    • encoder_asf::build_5_1_simple_asf_body_from_pcm_spectra(transform_length, max_sfb, max_sfb_lfe, &[&[f32]; 6], pad_target_bytes) — emits the full
      5.1 multichannel audio_data body for 5_X_codec_mode = SIMPLE,
      b_has_lfe = 1, coding_config = 3 (Cfg3Five): the round-74 5.0
      five_channel_data() payload is preceded by an LFE mono_data(1)
      element (no leading spec_frontend bit per Table 21,
      b_long_frame = 1, sf_info_lfe() with max_sfb_lfe in
      n_msfbl_bits bits, then a single
      (section + spectral + scalefac + snf) ASF body capped to the LFE
      band budget). At tl = 1920 n_msfbl_bits = 3, so the LFE channel
      spans at most 7 scalefactor bands (≈0–350 Hz) — comfortably more than
      the 120 Hz LFE crossover and the 60 Hz tone used by the new tests.
    • Ac4ImsEncoder::encode_frame_pcm_5_1(&[L, R, C, Ls, Rs, LFE]) and
      encode_frame_pcm_5_1_with_max_sfb(..., max_sfb, max_sfb_lfe)
      forces the 5.1 channel_mode prefix (0b1110, 4 b — Table 85
      channel_mode 4) so the decoder's walk_ac4_substream dispatches
      channels == 6 through parse_5x_audio_data_outer(b_has_lfe = true),
      runs the round-74 forward MDCT pipeline (KBD-windowed MDCT +
      DP-optimal sectioning + HCB1..11 + SNF) per channel, and wraps the
      body in the v2 IMS TOC with bitstream_version = 2.
    • Decoder LFE PCM render in Ac4Decoder::receive_frame: when
      channels == 6 (5.1) or channels == 8 (7.1) and the 5_X / 7_X
      walker populated tools.lfe_mono_data.scaled_spec, the LFE
      spectrum is IMDCT'd into the trailing PCM slot (slot 5 for 5.1,
      slot 7 for 7.1) using the per-channel overlap-add history.
      Pre-r80 the LFE block was parsed but its PCM was silently dropped.
    • Spectral SNR on the 220 / 440 / 660 / 880 / 1100 Hz independent-tone
      fixture matches the round-74 5.0 numbers
      (L=24.5 / R=24.8 / C=25.0 / Ls=23.4 / Rs=27.4 dB) and clears the ≥ 20 dB
      floor; the 60 Hz LFE tone round-trips to a non-silent reconstructed
      LFE channel. 7.0 / 7.1 (immersive add-channel pair) and the ASPX /
      A-CPL multichannel modes remain deferred.
    • New test suite tests/round80_5_1_multichannel.rs (7 tests) covers
      the layout, sequence-counter rolling, walker contract
      (b_has_lfe == true + populated lfe_mono_data.scaled_spec),
      silence round-trip, independent-tone round-trip with audible LFE, and
      per-channel SNR ≥ 20 dB.
  • Round 74 — 5.0 SIMPLE/ASF Cfg3Five multichannel forward analysis (5 SCE)
    encoder
    per ETSI TS 103 190-1 §4.2.6.6 Table 25 row
    case SIMPLE: coding_config == 3 + §4.2.7.5 Table 29 (five_channel_data())

    • §4.2.10.1 Table 47 (chparam_info()):
    • encoder_asf::build_5_0_simple_asf_body_from_pcm_spectra(transform_length, max_sfb, &[&[f32]; 5], pad_target_bytes) — emits the full 5.0
      multichannel audio_data body for 5_X_codec_mode = SIMPLE /
      coding_config = 3 (Cfg3Five): audio_size_value (15 b) +
      b_more_bits (1 b) + byte_align + 5_X_codec_mode = SIMPLE (3 b) +
      coding_config = 3 (2 b) + five_channel_data() (shared
      asf_transform_info + shared asf_psy_info + five_channel_info with
      chel_matsel = 0 + 5x chparam_info with sap_mode = 0 for identity
      SAP + 5x sf_data(ASF) bodies). No joint-MDCT mixing happens at decode
      time — every output channel comes straight from its own sf_data(ASF)
      body. SIMPLE has no aspx_config() / acpl_config_*() (those are
      I-frame I/O for ASPX modes only), no companding, no LFE
      mono_data(b_lfe=1) (5.0 → b_has_lfe = false).
    • Ac4ImsEncoder::with_5_0() — channel-mode prefix 0b1101 (4 b — Table
      85 channel_mode 3) for the 5.0 surround layout (L, R, C, Ls, Rs)
      without LFE.
    • Ac4ImsEncoder::encode_frame_pcm_5_0(&[&[f32]; 5]) +
      encode_frame_pcm_5_0_with_max_sfb() — accept paired L/R/C/Ls/Rs float
      PCM frames at the encoder's configured frame_len (1920 samples for the
      default 48 kHz / 24 fps), run the round-50 forward pipeline (KBD-
      windowed MDCT + per-band scalefactor + DP-optimal sectioning +
      HCB1..11 codebook selection + SNF emission) independently per channel,
      then emit a bitstream_version = 2 IMS TOC (channel_mode prefix
      '1101') followed by the 5.0 SIMPLE/Cfg3Five body. The encoder uses
      one [encoder_mdct::EncoderMdctState] per channel (new field
      mdct_states_multi: Vec<EncoderMdctState>) so 50% TDAC overlap
      continuity is preserved per channel.
    • The decoder's existing dispatch_5x_cfg3_simple_aspx path (round 39)
      consumes the body, IMDCTs each per-channel spectrum into output slots
      0..4 (L/R/C/Ls/Rs per Table 180 row coding_config == 3), and emits
      5-channel interleaved S16 PCM at the declared sample rate.
    • Round-trip SNR target met: ≥ 20 dB spectral SNR per channel on the
      independent-tone fixture (220 / 440 / 660 / 880 / 1100 Hz on L/R/C/Ls/Rs).
      Measured: L=24.5, R=24.8, C=25.0, Ls=23.4, Rs=27.4 dB — comfortably above
      the 20 dB floor and in the same band as the round-51 stereo Path A SNR
      (24.8 dB on 440 Hz). The decoder's 5-channel S16 interleaved layout
      (1920 × 5 × 2 = 19,200 bytes) round-trips cleanly through
      Ac4Decoder::receive_frame.
    • Seven new tests in tests/round74_5_0_multichannel.rs
      (round74_5_0_encoder_produces_5channel_layout_pcm,
      round74_5_0_encoder_bumps_sequence_counter,
      round74_5_0_encoder_toc_declares_5_channels,
      round74_5_0_independent_tones_per_channel_round_trip_with_distinct_audio,
      round74_5_0_per_channel_spectral_snr_exceeds_20db,
      round74_5_0_substream_parses_via_walk_ac4_substream,
      round74_5_0_silence_round_trips_to_silence).
    • 5.1 (channel_mode 0b1110 — adds LFE), 7.0/7.1 (channel_mode
      0b11110000/0b11110001 — adds front-extension / back-surround pair),
      and the ASPX/A-CPL multichannel modes (5_X_codec_mode in {ASPX, ASPX_ACPL_1, ASPX_ACPL_2, ASPX_ACPL_3}) are deferred. 5.0
      SIMPLE Cfg3Five is the spec-mandated minimum for 5-channel AC-4
      streams and unblocks the encoder's path to LFE / immersive layouts.
  • Round 52 — Joint M/S CPE (Path B: b_enable_mdct_stereo_proc == 1) encoder
    per ETSI TS 103 190-1 §5.3 (channel_count > 1) + §4.2.6.3 Table 22
    (stereo_data() with b_enable_mdct_stereo_proc == 1) + §7.5
    (Pseudocode 77 joint stereo, inverse M/S = L = M+S, R = M-S):

    • encoder_asf::average_per_sfb_correlation(transform_length, max_sfb, coeffs_l, coeffs_r) — energy-weighted per-SFB Pearson correlation
      between the L and R MDCT spectra. Bands are weighted by their
      geometric-mean energy sqrt(s_ll * s_rr) so spectrally-disjoint
      tones don't contaminate the metric. Returns a value in [-1.0, 1.0].
    • encoder_asf::build_stereo_simple_asf_joint_body_from_pcm_spectra( transform_length, max_sfb, coeffs_l, coeffs_r, pad_target_bytes)
      — emits the full joint stereo audio_data body:
      audio_size_value (15 b) + b_more_bits (1 b) + byte_align +
      stereo_codec_mode = SIMPLE (2 b) + b_enable_mdct_stereo_proc = 1 (1 b) + b_long_frame (1 b) + shared max_sfb (n_msfb_bits b)
      • shared asf_section_data() + per-channel asf_spectral_data()
        for M and S residuals + shared asf_scalefac_data() + per-active-
        sfb ms_used[sfb] (1 b each) + shared asf_snf_data().
    • Per-SFB M/S vs L/R decision: bit-cost compare between (M, S) and
      (L, R) at the baseline q_target=12 picks the cheaper representation
      band-by-band. The ms_used[sfb] flag on each active band tells the
      decoder whether to apply the inverse-M/S transform.
    • Frame-level "matched-channels" q_target bump: when the frame's
      total S energy is below 15% of M (e.g. for matched / near-matched
      stereo content), the M-channel anchor scalefactor is re-picked at a
      bumped peak-quant target (up to 16, smoothly tapering down to 12
      at the 15% threshold), spending the bits saved on the silent /
      near-silent S residual on a finer M quantisation. Per-band bumps
      were tried first but interact destructively with the shared joint-
      section cost table — when some bands are bumped and others aren't
      the joint scalefactor sequence misaligns the decoder's
      dequantisation. The frame-level gate keeps the bump self-
      consistent across the section partition.
    • Ac4ImsEncoder::encode_frame_pcm_stereo now dispatches between
      Path A (round 51, split-MDCT) and Path B (this round, joint M/S)
      based on the energy-weighted per-SFB correlation rising above
      Ac4ImsEncoder::STEREO_JOINT_MS_CORRELATION_THRESHOLD (0.7). New
      encode_frame_pcm_stereo_split_with_max_sfb /
      encode_frame_pcm_stereo_joint_with_max_sfb force a specific path
      regardless of correlation — used by tests / fixtures that need a
      deterministic on-wire layout.
    • Round-trip SNR targets met:
      • Matched 440 Hz L=R: ≥ 34.5 dB spectral SNR (vs round-51's
        24.8 dB on this fixture) — the q_target bump tightens the M
        quantisation step by ~half, S quantises to all-zero, and the
        decoder reconstructs L = M+0, R = M-0 with the bumped precision.
      • Independent 440 Hz L + 660 Hz R: routed via Path A (correlation
        0.0 by design), preserving round-51's ≥ 24.8 dB SNR floor.
      • Half-correlated stereo (amplitude-imbalanced 440 Hz at
        0.30 / 0.36): routed via Path B, frame-level S/M ratio ≈ 0.003
        triggers the q_target bump, output ≥ 26.4 dB / 28.0 dB per
        channel — between the pure-matched and fully-independent
        regimes.
    • Six new tests in tests/round52_joint_ms_stereo.rs
      (round52_matched_stereo_joint_ms_snr_exceeds_28db,
      round52_independent_stereo_routes_via_split_path_a,
      round52_half_correlated_stereo_joint_ms_snr_exceeds_26db,
      round52_joint_ms_full_pcm_roundtrip_through_ac4decoder) plus two
      in-module correlation sanity tests
      (round52_correlation_identical_channels_is_one,
      round52_correlation_independent_tones_below_threshold).
  • Round 51 — Stereo SIMPLE/ASF split-MDCT (Path A: 2× SCE) encoder
    per ETSI TS 103 190-1 §5.3 (channel_count > 1) + §4.2.6.3 Table 22
    (stereo_data() with b_enable_mdct_stereo_proc == 0):

    • Ac4ImsEncoder::encode_frame_pcm_stereo(frame_l, frame_r) and
      encode_frame_pcm_stereo_with_max_sfb() — accept paired L+R float
      PCM frames at the encoder's configured frame_len (1920 samples for
      the default 48 kHz / 24 fps), run the round-50 forward pipeline
      (KBD-windowed MDCT + per-band scalefactor + HCB1..11 codebook
      selection + DP-optimal section boundaries + SNF emission)
      independently per channel, then emit a bitstream_version = 2 IMS
      TOC (channel_mode prefix '10') followed by the split-MDCT stereo
      CPE body. The encoder uses separate EncoderMdctState per channel
      (new field mdct_state_r: Option<EncoderMdctState>) so 50% TDAC
      overlap continuity is preserved per channel.
    • encoder_asf::build_stereo_simple_asf_split_body_from_pcm_spectra( transform_length, max_sfb, coeffs_l, coeffs_r, pad_target_bytes)
      — emits the full stereo audio_data body: audio_size_value (15 b)
      • b_more_bits (1 b) + byte_align + stereo_codec_mode = SIMPLE (2 b) + b_enable_mdct_stereo_proc = 0 (1 b) + per-channel
        spec_frontend (1 b) + b_long_frame (1 b) + max_sfb (n_msfb_bits b) headers + per-channel sf_data(ASF) payloads (sections +
        spectral + scalefac + snf).
    • Round-trip SNR target met: ≥24.8 dB spectral SNR on both L and R
      for the 440 Hz matched-tone fixture and the 440 Hz L + 660 Hz R
      independent-tone fixture (PCM amplitude 0.3, 3 frames to reach
      steady state, comparison done in MDCT-spectrum domain to isolate
      the encoder quantisation contribution from IMDCT/KBD reconstruction
      phase shift). PCM peak ~10 400 i16 (= 0.317 amplitude, matching
      input).
    • Three new SNR / non-silence tests
      (encode_frame_pcm_stereo_440hz_both_channels_snr_exceeds_20db,
      encode_frame_pcm_stereo_440l_660r_independent_channels_snr_exceeds_20db,
      encode_frame_pcm_stereo_440hz_steady_state_nonsilent_both_channels)
      plus three structural smoke tests
      (encode_frame_pcm_stereo_bumps_sequence_counter,
      encode_frame_pcm_stereo_produces_stereo_layout_pcm,
      encode_frame_pcm_stereo_substream_parses).
    • Joint M/S coding (Path B — b_enable_mdct_stereo_proc == 1) and
      multichannel SAP/M-S decisioning are deferred. SIMPLE 2× SCE is
      the spec-mandated minimum for stereo AC-4 streams and unblocks the
      encoder's path to multichannel.