Skip to content

v0.0.5

Choose a tag to compare

@MagicalTux MagicalTux released this 10 May 03:22
· 55 commits to master since this release
7feced4

Other

  • ac4 round 50: section-boundary DP optimiser + SNF emission
  • ac4 round 49: HCB1..11 codebook-selection optimiser + parameterised max_sfb
  • ac4 round 48: forward MDCT + ASF entropy encoder for arbitrary PCM
  • ac4 round 47: IMS bitstream_version=2 TOC parser + mono SIMPLE/ASF tone encoder
  • round 46: AC-4 IMS encoder scaffold + ACPL_1 surround Ls/Rs spec audit
  • ac4 round 45: stereo-CPE M=2 synced companding for ACPL_3 surround pair
  • ac4 round 44: companding sync_flag=1 cross-channel exact synchronisation
  • ac4 round 43: companding sync_flag=1/avg branches + ACPL_1 sb0 hookup
  • ac4 round 42: cfg0/cfg1/cfg3 trailer-aware ASPX + §5.7.5 companding
  • ac4 round 41: 5_X cfg2 ASPX trailers + Table 181 SAP for ACPL_1
  • ac4 round 40: SAP a/b/c/d (Pseudocode 59) + Table 183 + Ls/Rs walker
  • ac4 round 39: 5_X cfg0/cfg1/cfg3 dispatch + 7_X additional-channel pair
  • ac4 round 38: LFE body decoder + cfg2_back_mono end-to-end + ACPL_3 centre
  • ac4 round 37: wire 7_X ACPL_1/_2 dispatch + cfg0 centre end-to-end decode
  • ac4 round 36: wire 5_X ASPX_ACPL_1 / ACPL_2 Pseudocode 117 into decoder
  • Round 35: extend ETSI validation suite to float reference tables
  • drop dead linkme dep
  • round 35 — EMDF payloads_substream parser + DRC PCM gain application
  • cargo fmt pass after round 34
  • update round 34 status (SNF + FIXVAR/VARFIX/VARVAR atsg + ACPL_3)
  • ac4 round 34: FIXVAR/VARFIX/VARVAR atsg + SNF inject + 5_X ACPL_3 wiring
  • auto-register via oxideav_core::register! macro (linkme distributed slice)
  • unify entry point on register(&mut RuntimeContext) (#502)

Added

  • Round 50 — Section-boundary DP optimiser + Spectral Noise Fill
    (SNF) emission
    (TS 103 190-1 §5.7.4 section_data + §5.7.6 SNF +
    Pseudocodes 100 + 105 + Table 39/42 + Table SCFB):

    • encoder_asf::dp_optimise_sections(cost_band_cb, max_sections)
      dynamic-program over scale-factor bands that finds the globally
      cheapest sequence of (start, end, cb) sections, paying the
      per-section header cost (4 + 3 * (floor((L-1)/7)+1) bits per
      Table 39) against each band's per-codebook bit cost. Section
      count capped at 16 per ETSI Table SCFB. Supersedes the round-49
      greedy run-length codebook-merge optimiser.
    • encoder_asf::section_overhead_bits(len) — closed-form per-section
      overhead in bits matching the spec's n_sect_bits=3 / esc=7
      long-frame layout: 7 bits for L ∈ 1..=7, 10 bits for L ∈ 8..=14,
      13 bits for L ∈ 15..=21, etc.
    • encoder_asf::build_band_codebook_cost_table(natural_q_per_band)
      — precomputed per-band per-codebook bit cost (rows of length 12;
      cb=0 cost 0 only for all-zero bands; HCB1..11 costs from
      bit_cost_for_band; u32::MAX for codebooks that can't represent
      the band's natural quant magnitudes). Drives the DP via O(1)
      prefix sums where every band is feasible for the codebook.
    • encoder_asf::build_sections_from_dp(sections, max_sfb) — lowers
      the DP-derived (start, end, cb) triples into an AsfSections
      suitable for the existing write_section_data /
      write_spectral_data_sections emitters.
    • encoder_asf::compute_snf_dpcm_for_zero_quant_bands(coeffs, sfb_offset, max_sfb, sfb_cb, max_quant_idx) — for each band that
      quantises to all-zero (cb == 0 || mqi == 0), estimates the band's
      RMS energy from the original MDCT coefficients and picks the
      HCB_SNF index whose snf_gain = 2^((idx*1.5 - 84)/4) best matches.
      Returns Some(per_band_idx) when at least one zero-quant band
      has measurable energy; None for fully-silent input.
    • encoder_asf::write_snf_data(bw, snf, sfb_cb, max_quant_idx, max_sfb)
      — emits b_snf_data_exists (1 bit) plus per-zero-quant-band
      HCB_SNF Huffman codewords per Table 42 / Pseudocode 105. Round-trips
      cleanly through the existing parse_asf_snf_data decoder path
      (round 36+).
    • encoder_asf::measure_greedy_vs_dp_bits(transform_length, max_sfb, coeffs) — diagnostic helper returning (greedy_bits, dp_bits)
      for any input spectrum so callers can quantify the section-boundary
      optimiser's contribution to total frame size.
    • Ac4ImsEncoder::encode_frame_pcm_with_max_sfb now drives the DP
      optimiser + SNF emission internally; existing call sites get the
      new path automatically with no API change. White-noise spectral
      SNR holds at 27.5 dB (round-49 baseline) with section overhead
      reduced and SNF emission turned on for high-frequency zero-quant
      content.
  • Round 49 — Codebook-selection optimiser (HCB1..11) + parameterised
    max_sfb
    (TS 103 190-1 §5.7 + Pseudocodes 17 + 19 + 20 + Annex A.0
    huff_codes + Table SCFB):

    • encoder_asf::pick_best_codebook_for_band — per-band codebook
      optimiser sweeping HCB1..11 and choosing the lowest-bit-cost
      codebook whose q_max covers the band's natural quantised range.
      Anchor scalefactor targets peak quant ≈ 12 (HCB9/10's q_max → 3×
      more quantisation levels per band than the round-48 HCB5-only
      baseline). HCB11 always qualifies via its Pseudocode 20 escape so
      very-high-energy bands don't clip.
    • encoder_asf::bit_cost_for_band — precise bit-counter modelling
      HCB1..11 codeword length + sign bits for unsigned codebooks +
      per-Pseudocode-20 n_ext extension bits for HCB11 escapes.
      Mirrors the encoder emitter's exact bit shape (inline magnitude
      saturates at 16 for HCB11, sign bit per non-zero post-saturation
      line for unsigned codebooks).
    • encoder_asf::build_sections_from_per_band_cb — collapses runs
      of consecutive same-codebook bands into a single AsfSections
      entry so the emitted asf_section_data() honours the spec's
      grouping pseudocode without spurious cb-switch overhead.
    • encoder_asf::write_section_data + write_spectral_data_sections
      — multi-section asf_section_data + asf_spectral_data emitters.
      Per-section emission walks sect_start..sect_end bins with the
      section's codebook, handles cb == 0 silent bands, and writes
      Pseudocode 20 escape bits for HCB11 outliers.
    • Ac4ImsEncoder::encode_frame_pcm_with_max_sfb(frame, max_sfb)
      new public entry point parameterising max_sfb (round-48 default
      was hard-coded to 40 → ~6.4 kHz at tl=1920 / 48 kHz). Pad target
      scales with max_sfb (2KB / 4KB / 8KB tiers). encode_frame_pcm
      keeps the round-48 default of max_sfb=40 for backwards
      compatibility.
    • White-noise round-trip SNR jumps from 13.6 dB (round-48 HCB5-only
      baseline) to 27.5 dB (round-49 HCB1..11 optimiser, q_target=12,
      max_sfb=50) — measured spectrally against the encoder's own MDCT
      coefficients pre/post quantisation. 1 kHz tone reconstruction at
      max_sfb=55 preserves >100% of input energy (vs ~40% at the
      round-48 max_sfb=40 default).
  • Round 48 — Forward MDCT analysis + ASF entropy encoder for arbitrary
    PCM input
    (TS 103 190-1 §5.5 MDCT + §5.7 SIMPLE + §5.8 ASF +
    Pseudocodes 17-19 + Annex A.0 huff_codes):

    • encoder_mdct::mdct_naive + EncoderMdctState — forward MDCT
      direction complementing the decoder's mdct::imdct. Naive
      O(N²) direct-summation cosine basis (correctness-first; encoder
      isn't on a hot path). Sign convention + scaling matched against
      the decoder's IMDCT through a Princen-Bradley TDAC round-trip
      test (constant-signal recovery in steady-state middle frame
      within 1% error). EncoderMdctState carries the previous-frame
      N PCM samples for cross-frame 50% TDAC overlap.
    • encoder_asf::quantise_coeff + pick_scalefactor_for_band +
      encode_pair + write_sect_len_incr + write_scalefac_data +
      build_mono_simple_asf_body_from_pcm_spectrum — closed-form
      forward ASF entropy encoder for the long-frame, single-window-
      group, mono SIMPLE channel case. Per-band scalefactor selection
      via the closed-form solve sf_min = ceil(100 + 4*log2(max_abs/q_max^(4/3)))
      keeping every quantised line within HCB5's ±4 magnitude bound
      after q = round(sign(c)*|c/sf_gain|^(3/4)). Single-section
      HCB5 emission across 0..max_sfb; reference scalefactor
      • DPCM-coded per-band deltas via HCB_SCALEFAC; b_snf_data_exists = 0.
    • Ac4ImsEncoder::encode_frame_pcm(input: &[f32]) — public entry
      point taking arbitrary float PCM input (range [-1.0, 1.0])
      and emitting a structurally-valid IMS v2 frame end-to-end:
      forward MDCT analysis → per-band scalefactor + quantisation →
      HCB5 entropy coding → wrap in v2 IMS TOC + audio_size header.
      Lazily initialises a per-encoder EncoderMdctState on first
      call; bumps sequence_counter modulo 1024. Mono / 48 kHz / 24 fps
      by default (frame_len = 1920 samples, max_sfb = 40 covering
      bins 0..600 ≈ 7.5 kHz).
    • 13 new unit tests across the three modules cover: forward MDCT
      zero-in-zero-out + linearity + Princen-Bradley constant-signal
      • sine-wave SNR > 40 dB; quantise/dequantise round-trip;
        pick_scalefactor q_max bound; encode_pair signed/unsigned
        round-trip via huff_decode + split_qspec;
        build_mono_simple_asf_body_from_pcm_spectrum end-to-end parse
        via the existing ASF decoder; full encode → decode round-trip
        for 1 kHz tone (peak amplitude > 1000 i16), multi-tone
        250+500+1000 Hz (SNR > 10 dB on steady-state frame), and
        silence (peak < 50 i16); encode_frame_pcm bumps
        sequence_counter per call.
    • Codebook-selection optimiser (try HCB1..11 per section, pick
      min-bits), section-boundary optimiser (split bands by
      codebook), spectral noise fill, and stereo / multichannel
      forward analysis remain deferred for round 49+.
  • Round 46 — AC-4 IMS encoder scaffold + ACPL_1 surround Ls/Rs
    ASPX-extension spec audit
    (TS 103 190-2 §6.2.1.1 / §6.3.2.5,
    TS 103 190-1 §4.2.6.6 Table 25):

    • encoder_ims::Ac4ImsEncoder — Auditor-mode AC-4 IMS encoder
      skeleton. Emits a structurally-valid raw_ac4_frame() payload
      with the IMS-flavoured ac4_toc() (bitstream_version = 2 +
      ac4_presentation_v1_info() + ac4_substream_group_info()) per
      TS 103 190-2 §6.2.1.1. Public API: Ac4ImsEncoder::new() (defaults
      to mono 48 kHz 24 fps single-presentation single-substream-group
      iframe), with_v0() / with_stereo() / with_5_1() builders,
      encode_frame(body_padding_bytes) (bumps the 10-bit
      sequence_counter modulo 1024), and encode_frame_v0(...) that
      forces the TS 103 190-1 v0 layout for round-trip-with-decoder
      tests. Audio body is zero-byte placeholder bits — full encoder
      pipeline (MDCT analysis, scalefactor selection, entropy coding,
      A-SPX envelope coding, A-CPL parameter extraction) deferred to
      future rounds. Eight new unit tests cover sequence_counter wrap,
      parse_ac4_toc round-trip for mono / stereo / 5.1, full
      Ac4Decoder round-trip emitting silent audio, and the leading
      bitstream_version bit-pattern invariant for both v0 (0b00)
      and v2 (0b10) frames.
    • decoder.rs::dispatch_acpl_5x_pair-driving block: spec-confirms
      NOT-ASPX finding for the ACPL_1 surround Ls/Rs carriers per ETSI
      TS 103 190-1 §4.2.6.6 Table 25 row case ASPX_ACPL_1:. The
      trailer order is aspx_data_2ch() (L/R primary pair) +
      aspx_data_1ch() (centre mono) + two acpl_data_1ch() parameter
      sets — there is NO third ASPX trailer for the surround pair. The
      Ls/Rs sSMP,3 / sSMP,4 carriers are joint-MDCT residuals that feed
      Pseudocode 117 raw, with the post-Pseudocode-117 surround-output
      bandwidth shape coming from the L/R-carrier ACPL synthesis
      (alpha/beta/decorrelator), not from independent surround-pair
      extension. Same finding for the M=2 surround-pair synced
      companding cohort: with no surround carriers, no companding to
      sync. The existing raw-PCM dispatch path is correct per spec —
      no behavioural change in this round, only a documentation note
      closing out the round-45 follow-up flagged in dispatch_acpl_5x_pair.
  • Round 44 — companding sync_flag == 1 cross-channel synchronisation
    (Pseudocode 121's exact g_synch(ts) = (∏ g_ch)^(1/M))
    (TS 103
    190-1 §5.7.5.2 + Pseudocode 121):

    • aspx::compute_companding_levels(q, sb0, sb1) exposes the
      per-slot energy L_ch(ts) = 0.9105 * mean E_ch(sb,ts) without
      applying gain — the building block the synced path collects
      across channels.
    • aspx::levels_to_scales_per_slot(levels) and
      aspx::levels_to_scale_averaged(levels) produce the per-slot
      array (PerSlot / SyncPerSlot) and single constant (Averaged /
      SyncAveraged) scales g(ts) * G from a level vector.
    • aspx::apply_companding_scales_on_qmf(q, sb0, sb1, scales)
      applies pre-computed scales — used by both the single-channel
      legacy path and the synced multi-channel path.
    • aspx::apply_synchronised_companding_across_channels(channels, mode)
      implements Pseudocode 121's sync_flag == 1 branch directly:
      collects every channel's L_ch(ts), computes g_synch(ts) as
      the geometric mean across M channels (SyncPerSlot) or
      g_avg,synch from per-channel averaged levels (SyncAveraged),
      then applies the synced gain g_synch(ts) * G UNIFORMLY to
      every contributing channel. Geometric mean is computed via
      log-sum / exp for numerical stability across many channels.
    • Ac4Decoder::aspx_extend_to_qmf(...) is the new phase-1 split:
      runs the QMF analysis + HF generation + envelope adjustment +
      noise / tone injection, returns (qmf_matrix, sbx, sbz) BEFORE
      companding + synthesis. Returns None when the QMF
      preconditions trip (length / table / patch derivation).
    • Ac4Decoder::qmf_synthesise_pcm(q, out_len) is the phase-2
      split: runs the inverse QMF synthesis and returns PCM. Caller
      is responsible for having applied any §5.7.5 gain.
    • Ac4Decoder::extend_5x_channels_with_sync_companding(...) is
      the integration glue: drives every entry through phase-1,
      collects QMF matrices, calls the synced apply, drives every
      entry through phase-2. Channels whose phase-1 returned None
      fall through to the unmodified PCM (matching the per-channel
      aspx_extend_pcm contract).
    • Ac4Decoder::extend_5x_entries(...) is the shared front-end
      used by dispatch_5x_cfg{0,1,2,3}_simple_aspx: routes through
      the synced cross-channel path when companding.sync_flag == 1,
      otherwise through the per-channel path.
    • Ac4Decoder::five_x_synced_mode(cc) resolves the synced mode
      once for the whole 5_X frame (Pseudocode 121 broadcasts
      compand_on[0] to every channel under sync_flag == 1).
    • All four dispatch_5x_cfg{0,1,2,3}_simple_aspx routes
      refactored to build a per-channel entries list then route
      through extend_5x_entries. Cfg2 now also folds the centre
      channel into the synced cohort when back_mono is present, so
      M == 5 for the geometric mean across all five 5_X channels
      (was M == 4 in the round-43 approximation).
    • The round-43 single-channel approximation (SyncPerSlot /
      SyncAveraged collapsing to per-channel g_ch / g_avg,ch)
      is RETIRED — every 5_X SIMPLE/ASPX dispatcher now applies the
      exact synchronised gain across all contributing channels.
  • Round 43 — companding b_compand_avg + sync_flag == 1 branches +
    ASPX_ACPL_1 sb0 = acpl_qmf_band hookup
    (TS 103 190-1 §5.7.5.2 +
    Pseudocode 121):

    • aspx::CompandingMode enum captures the (sync_flag,
      b_compand_on[ch], b_compand_avg) product per Pseudocode 121:
      Off, PerSlot, Averaged, SyncPerSlot, SyncAveraged.
    • aspx::CompandingMode::from_control(cc, slot) resolves the
      active branch for a single channel from a parsed
      CompandingControl, honouring sync_flag == true (slot 0
      broadcasts) and the b_compand_avg presence-rule.
    • aspx::apply_companding_on_qmf_with_mode(q, sb0, sb1, mode)
      extends the round-42 single-channel path to all four active
      sub-branches: PerSlot / SyncPerSlot apply g_ch(ts) * G
      per timeslot; Averaged / SyncAveraged average L_ch(ts)
      over the full A-SPX interval, derive a single constant
      g_avg,ch * G and broadcast it across all timeslots. (Off
      is a strict no-op.) apply_companding_on_qmf retained as a
      PerSlot thin wrapper for backward compatibility.
    • Ac4Decoder::aspx_extend_pcm signature now takes
      (compand_mode: CompandingMode, compand_sb0_override: Option<u32>)
      instead of a raw compand_on: bool. The override implements
      §5.7.5.2's sb0 selection rule: for the ASPX_ACPL_1 codec mode
      sb0 = acpl_qmf_band (from acpl_config_1ch_partial.qmf_band);
      otherwise sb0 falls back to tables.sbx (= aspx_xover_band).
    • Stereo CPE path (receive_frame) lifts the override from
      tools.acpl_config_1ch_partial.qmf_band whenever the active
      stereo or 5_X mode is AspxAcpl1. Cfg0/Cfg1/Cfg2/Cfg3 SIMPLE/ASPX
      dispatchers pass None (those paths never run on ACPL_1).
    • Ac4Decoder::five_x_compand_mode_for_slot resolves a
      CompandingMode per output slot for the 5_X dispatchers; the
      legacy five_x_compand_on_for_slot is retained as a thin
      mode != Off wrapper for round-42 unit-test compatibility.
    • sync_flag == 1 cross-channel synchronisation: the per-channel
      pipeline computes g_ch(ts) per channel; for M = 1 (the
      dominant case in our pipeline) the geometric mean across
      channels reduces to the local gain (exact). For M > 1 channels
      processed independently the synchronisation is approximated —
      documented as a known limitation in
      apply_companding_on_qmf_with_mode.
    • 5_X ACPL_3 path companding wiring: already in place from round
      42 via the stereo CPE path's compand_mode_pri /
      compand_mode_sec (the ACPL_3 walker populates tools.companding
      from companding_control(2); the L/R carriers go through the
      standard stereo CPE primary/secondary path which now lifts
      CompandingMode and applies it before the §5.7.7.6.2
      Pseudocode 118 multichannel synthesis).
    • 7 new tests cover: CompandingMode::from_control resolves all
      six sync/on/avg product states; apply_companding_on_qmf_with_mode
      Off strict no-op + Averaged/SyncAveraged constant-scale
      invariant + sb0-override band shift; five_x_compand_mode_for_slot
      branch resolution; aspx_extend_pcm with sb0 override + with
      Averaged mode diverges from baseline.
  • Round 42 — 5_X SIMPLE/ASPX cfg0/cfg1/cfg3 trailer-aware ASPX
    dispatch + §5.7.5 companding tool
    (TS 103 190-1 §4.2.6.6 / §5.7.5):

    • asf::SubstreamTools gained cfg0_aspx_{lr,ls_rs,centre},
      cfg1_aspx_{lr,ls_rs,centre}, cfg3_aspx_{lr,ls_rs,centre}
      mirrors of the round-41 cfg2 trailer slots. The 5_X SIMPLE/ASPX
      outer walker now stores the captured Table-25 case ASPX:
      trailer triplet (aspx_data_2ch + aspx_data_2ch + aspx_data_1ch)
      into the slot matching the active coding_config instead of
      discarding round-41's cfg0/1/3 captures.
    • Ac4Decoder::dispatch_5x_cfg{0,1,3}_simple_aspx now apply the
      A-SPX bandwidth-extension per output channel using the captured
      trailers, with the canonical Table-25 trailer-to-slot mapping
      (1st-2ch -> L/R, 2nd-2ch -> Ls/Rs, 1ch -> C). Independent of
      cfg0's b_2ch_mode (ASPX is applied after the channel-element
      decode produces PCM, so the L,Ls / R,Rs inner stereo coding
      doesn't shuffle the trailer assignment).
    • Ac4Decoder::maybe_extend_5x_slot — internal helper that picks
      the right (trailer, primary/secondary) for an output slot and
      runs aspx_extend_with_trailer with the resolved companding-on
      flag for that slot.
    • aspx::apply_companding_on_qmf(q, sbx, sbz) — §5.7.5.2
      companding tool decoder side, applied per-channel on the QMF
      matrix in [sbx, sbz) for all timeslots. Implements the
      sync_flag == 0, b_compand_on == true branch (the dominant
      case): per-slot mean-absolute level L_ch(ts) per Pseudocode
      equations, then g_ch(ts) = L^((1-α)/α) with α = 0.65 and
      Q_out = g * G * Q_in with G = 2^α. Zero-signal slot uses
      unit gain (avoids 0^negative_exp = inf). The b_compand_avg
      averaging and the sync_flag == 1 per-channel-product synched
      gain branches are scaffolded for a later round.
    • Ac4Decoder::aspx_extend_pcm extended with a compand_on: bool
      parameter — applies apply_companding_on_qmf between envelope
      adjustment and inverse QMF synthesis when set. All call sites
      updated: stereo CPE primary/secondary (uses
      tools.companding[0] / [1]), and every 5_X cfg0/1/2/3 slot
      (uses the companding_control(5) slot lookup via
      five_x_compand_on_for_slot).
    • Ac4Decoder::five_x_compand_on_for_slot(cc, slot) — resolves
      b_compand_on[slot] from a CompandingControl, honouring
      sync_flag == true (slot 0 broadcasts), per-channel, and absent
      flags (returns false).
    • 5 new tests cover: per-cfg trailer-aware dispatch produces
      output that diverges from the round-39 low-band-only path;
      companding flag resolver across mono / per-channel / sync
      branches; aspx_extend_pcm with companding diverges from
      baseline; and edge-case no-ops for apply_companding_on_qmf
      (degenerate band + zero signal).
  • Round 41 — 5_X SIMPLE/ASPX cfg2 ASPX bandwidth-extension trailers +
    Table 181 first-stage SAP matrix for 5_X / 7_X ASPX_ACPL_1
    (TS
    103 190-1 §4.2.6.6 / §5.3.4.3.2):

    • aspx::FiveXAspxTrailer + aspx::FiveXAspxChannelTrailer
      captured per-trailer bitstream state (xover, frequency tables,
      framing, qmode, delta-dir, sig/noise envelopes, hfgen
      add_harmonic / tna_mode) for one aspx_data_2ch() or
      aspx_data_1ch() payload. Wraps everything aspx_extend_pcm
      needs for one or two channels.
    • asf::capture_aspx_data_2ch_trailer /
      asf::capture_aspx_data_1ch_trailer — wrap parse_aspx_data_*_body
      with snapshot/restore over the per-substream ASPX-trailer slots
      so multiple sequential trailers can be parsed without corrupting
      each other's state.
    • parse_5x_audio_data_outer SIMPLE/ASPX branch now walks the
      Table 25 row case ASPX: trailer triplet (aspx_data_2ch + aspx_data_2ch + aspx_data_1ch) and stores the captured trailers
      on tools.cfg2_aspx_lr / cfg2_aspx_ls_rs / cfg2_aspx_centre.
      Cfg0/Cfg1/Cfg3 still parse the bits (so the bitreader lands at
      the right offset) but don't yet wire to dispatch.
    • Ac4Decoder::dispatch_5x_cfg2_simple_aspx extended to apply
      A-SPX bandwidth-extension per-channel: L/R use aspx_lr (primary
      / secondary); Ls/Rs use aspx_ls_rs; centre uses aspx_centre.
      SIMPLE-mode (no trailers) and trailer-parse-miss paths fall
      through to round-38 low-band-only PCM.
    • asf::apply_sap_table_181(a, b, s3, s4, chparam_pair, max_sfb, tl) -> (l, r, ls, rs) — Table 181 first-stage matrix for
      5_X_codec_mode == ASPX_ACPL_1. Mixes (sSMP_A, sSMP_B) with
      (sSMP_3, sSMP_4) per-sfb using the (a, b, c, d) coefficients
      extracted from each chparam_info() payload (Pseudocode 59) into
      preliminary (L, R, Ls, Rs) spectra. Bands past max_sfb_master
      pull L/R from A/B and zero Ls/Rs.
    • parse_aspx_acpl_1_2_inner_body (5_X) +
      parse_7x_audio_data_outer (7_X ASPX_ACPL_1 branch) now persist
      the parsed chparam_info() pair on
      tools.acpl_1_residual_chparam plus max_sfb_master on
      tools.acpl_1_residual_max_sfb_master — round 40 already
      persisted the residual spectra; round 41 closes the loop with
      the SAP coefficients + bound.
    • Ac4Decoder::receive_frame 5_X ACPL_1 dispatch now applies
      Table 181's SAP matrix in the spectral domain when all four
      inputs (sSMP_A, sSMP_B, sSMP_3, sSMP_4 + chparam pair +
      max_sfb_master) are available, IMDCTs the resulting (L, R, Ls,
      Rs) preliminary spectra, and feeds them into Pseudocode 117
      (run_acpl_5x_pair_pcm) as the already-mixed PCM inputs the
      spec expects. ACPL_2 mode (no residual pair) falls through to
      round-40 silence placeholders.
  • Round 40 — SAP a/b/c/d coefficient extraction (Pseudocode 59) +
    Table 183 7_X SIMPLE/ASPX final channel mapping + standalone Ls/Rs
    surround mono walker for ACPL_1 Mode 1
    (TS 103 190-1 §5.3.2 /
    §5.3.4.4.1 / §5.7.7.6.1):

    • asf::SapCoeffs + asf::extract_sap_abcd(info, max_sfb_per_group)
      — Pseudocode 59 implementation. Walks chparam_info.sap_mode
      {0, 1, 3} and emits per-(g, sfb) (a, b, c, d) quartets:
      • sap_mode == 0 → identity (a=d=1, b=c=0).
      • sap_mode == 1, ms_used → M/S inverse (a=b=c=1, d=-1) per-sfb;
        identity where ms_used == false.
      • sap_mode == 3 → alpha-driven SAP. Pair-major DPCM differential
        decode of dpcm_alpha_qalpha_q[g][sfb] (odd sfbs inherit
        the even pair-mate; cross-group delta_code_time folds in when
        g != 0 and max_sfb_g == max_sfb_prev). `sap_gain = alpha_q
        • 0.1drives(a, b, c, d) = (1 + sap_gain, 1, 1 - sap_gain, -1)for SAP-coded bands;(1, 0, 0, 1)` for skipped bands.
    • Ac4Decoder::dispatch_7x_additional_channel_pair extended:
      accepts partner_pair_spectra + partner_slots + chparam. With
      b_use_sap_add_ch == true AND partner spectra of matching
      transform length, applies Table 183's SAP matrix per-sfb in the
      spectral domain — [out_high; out_low] = [a b; c d] · [partner; add_ch] — then IMDCTs both halves independently. With identity
      SAP the partner slot is left untouched (its independent 5_X-core
      IMDCT renders elsewhere) and only F/G land at slots 5/6 per Table
      182 — round-39 behaviour preserved.
    • Ac4Decoder::receive_frame 7_X branch resolves the partner pair
      from the active five_x_coding_config: cfg2 picks
      four_channel_data[2,3] (Ls/Rs); cfg3 picks
      five_channel_data[3,4]; cfg1 picks the trailing
      two_channel_data[0,1]. cfg0 has no natural surround partner
      inside the 5_X core — falls through to identity passthrough.
    • Standalone Ls/Rs surround mono walker for ACPL_1's Mode 1
      surround-driven path
      : parse_aspx_acpl_1_2_inner_body (5_X)
      and parse_7x_audio_data_outer (7_X) now persist the joint-MDCT
      residual pair (sSMP,3 / sSMP,4 per Table 181) on
      tools.acpl_1_residual_pair: [Option<(u32, Vec<f32>)>; 2]. The
      decoder's 5_X / 7_X ACPL_1 dispatch IMDCTs them into Ls / Rs PCM
      carriers and feeds them as the x3 / x4 inputs of Pseudocode
      117 — replacing the round-37 silence placeholder for slots 3 / 4.
      ASPX_ACPL_2 mode never emits a residual pair (no max_sfb_master
      in its walker), so the detach is None and surround stays at
      silence as before.
    • New tests (+8): extract_sap_abcd_mode_zero_returns_identity,
      extract_sap_abcd_mode_one_swaps_per_sfb_on_ms_used,
      extract_sap_abcd_mode_three_pair_dpcm_decode,
      extract_sap_abcd_mode_three_unused_bands_pass_through,
      extract_sap_abcd_mode_three_delta_code_time_cross_group,
      sap_coeffs_identity_helper,
      dispatch_7x_additional_pair_sap_identity_routes_partner_and_additional,
      dispatch_acpl_5x_pair_with_real_ls_rs_carriers_emits_surround_energy.
      The existing
      parse_5x_aspx_acpl_1_non_iframe_walks_three_channel_data grew an
      assertion that tools.acpl_1_residual_pair[0/1].is_some() after
      the inner walker runs. 568 tests total (was 560).
  • Round 39 — 5_X SIMPLE/ASPX cfg0/cfg1/cfg3 dispatch helpers +
    7_X SIMPLE/ASPX additional-channel pair render
    (TS 103 190-1
    §5.3.4.3.1 / Table 180 columns 0/1/3 + §5.3.4.4.1 / Table 182):

    • Ac4Decoder::dispatch_5x_cfg0_simple_aspx — IMDCTs each
      two_channel_data.scaled_spec_per_channel[0..2] into PCM slots
      per the 1-bit b_2ch_mode: false -> tcd_a→[0,1] (L,R) /
      tcd_b→[3,4] (Ls,Rs); true -> tcd_a→[0,3] (L,Ls) /
      tcd_b→[1,4] (R,Rs). cfg0_centre_mono.scaled_spec (the trailing
      mono_data(0)) lands on slot 2 (C).
    • Ac4Decoder::dispatch_5x_cfg1_simple_aspx — IMDCTs
      three_channel_data[0..3] into slots 0/1/2 (L/R/C) and
      two_channel_data[0..2] into slots 3/4 (Ls/Rs).
    • Ac4Decoder::dispatch_5x_cfg3_simple_aspx — IMDCTs
      five_channel_data[0..5] straight into slots 0..4 (L/R/C/Ls/Rs).
    • Ac4Decoder::dispatch_7x_additional_channel_pair — IMDCTs the
      seven_x_additional_channel_data.scaled_spec_per_channel[0..2]
      into PCM slots 5 / 6 (the F / G preliminary outputs in Table 182).
      SAP companding (Table 183 a,b,c,d) is the identity for now —
      b_use_sap_add_ch == false collapses the matrix; the explicit
      SAP coefficient extraction lands in a future round.
    • Ac4Decoder::receive_frame switch: round 38's cfg2-only branch
      now selects across Cfg0/Cfg1/Cfg2/Cfg3 based on the parsed
      five_x_coding_config. Mutually exclusive with the ACPL_3 / pair
      paths via the existing five_x_simple_aspx_active gate. The
      7_X SIMPLE/ASPX path additionally runs the additional-channel
      pair dispatch on top of the core 5-channel mapping.
    • New tests (+9): dispatch_5x_cfg0_populates_l_r_c_ls_rs_default_2ch_mode,
      dispatch_5x_cfg0_alternate_2ch_mode_maps_to_l_ls_r_rs,
      dispatch_5x_cfg1_populates_l_r_c_ls_rs,
      dispatch_5x_cfg3_populates_l_r_c_ls_rs,
      dispatch_5x_cfg013_noop_on_length_mismatch,
      dispatch_7x_additional_pair_populates_slots_5_and_6,
      dispatch_7x_additional_pair_noop_on_length_mismatch,
      plus integration tests
      five_x_simple_cfg0_walker_populates_two_two_plus_centre,
      five_x_simple_cfg3_walker_populates_five_channel_data. Total:
      551 -> 560.
  • Round 38 — LFE body decoder + cfg2_back_mono end-to-end decode +
    ACPL_3 centre channel via IMDCT
    (TS 103 190-1 §4.2.7.2 / §4.2.6.6
    Cfg2 / §5.7.7.6.2):

    • parse_mono_data(b_lfe=true) now walks the trailing sf_data(ASF)
      body via the new decode_asf_long_lfe_body_with_max_sfb_lfe
      helper. sf_info_lfe() (Table 35) forces long-frame / single
      window group, so the LFE body shape is the regular ASF long-frame
      quartet (`asf_section_data + asf_spectral_data + asf_scalefac_data
      • asf_snf_data) — only the max_sfb[0] bit-width changes (n_msfbl_bitsfrom Table 106 column 4 instead ofn_msfb_bits). The dequantised + scaled spectrum lands on MonoLfeData::scaled_spec`,
        matching the round-37 non-LFE behaviour.
    • Ac4Decoder::dispatch_5x_cfg2_simple_aspx — new helper that runs
      end-to-end IMDCT + overlap-add for the 5_X SIMPLE/ASPX
      coding_config == 2 channel layout. Walks the parsed
      four_channel_data.scaled_spec_per_channel[0..4] into output
      slots 0/1/3/4 (L/R/Ls/Rs per Table 180) and the trailing
      cfg2_back_mono.scaled_spec into slot 2 (C). Fires when
      five_x_mode in {Simple, Aspx} and five_x_coding_config == Cfg2FourMono. Cfg0 / Cfg1 / Cfg3 dispatch helpers remain deferred.
    • Ac4Decoder::receive_frame ACPL_3 path: replaces the round-37
      silence-placeholder for the centre carrier with an IMDCT of
      cfg0_centre_mono.scaled_spec via imdct_mono_lfe_data_f32. The
      Pseudocode 118 multichannel synthesis now emits a non-silent
      centre when the trailing mono_data(0) body decoded successfully;
      falls back to silence when the centre body is absent (LFE / SSF /
      Huffman miss / non-Cfg0 frame).
    • New unit + integration tests:
      • parse_mono_data_lfe_walks_sf_data_body — replaces the round-37
        "LFE body deferred" test; verifies the LFE body now decodes into
        scaled_spec for an all-zero stream.
      • dispatch_5x_cfg2_populates_l_r_c_ls_rs — cfg2 dispatch IMDCTs
        every channel into the right output slot with non-zero energy
        from per-channel ramp spectra.
      • dispatch_5x_cfg2_noop_on_length_mismatch — cfg2 dispatch
        leaves output slots untouched when the carrier transform length
        differs from the requested sample count.
      • five_x_simple_cfg2_walker_populates_four_plus_back_mono
        integration test threading parse_5x_audio_data_outer → cfg2
        tools layout, asserting the four per-channel scaled spectra +
        back-mono body all populate cleanly.
  • Round 37 — 7_X ASPX_ACPL_1 / ASPX_ACPL_2 dispatch + cfg0 centre
    end-to-end decode
    (TS 103 190-1 §5.7.7.6.3 Pseudocode 120 +
    §5.7.7.6.1 Pseudocode 117 cfg0 centre-channel wiring):

    • Ac4Decoder::receive_frame now dispatches the §5.7.7.6.3
      Pseudocode 120 channel-pair multichannel synthesis when the 7_X
      walker resolved seven_x_mode to AspxAcpl1 or AspxAcpl2. The
      dispatch reuses dispatch_acpl_5x_pair (the Pseudocode 117
      channel-pair core that Pseudocode 120 wraps for 7.X) — for
      ASPX_ACPL_{1,2} the additional 2 channels (z6/z7 in Pseudocode
      120) live outside the A-CPL pair so the 5-channel core (L/R/C/Ls/Rs)
      is identical between 5_X and 7_X. Slots 5..7 stay at silence until
      the additional-channel decode path lands.
    • MonoLfeData (mch::parse_mono_data) now decodes the trailing
      sf_data(ASF) body for non-LFE, ASF-frontend, long-frame /
      single-window-group mono channels — replacing the round-36
      silence-placeholder for the centre channel in the 5_X / 7_X
      Pseudocode 117 / 120 dispatches with a real IMDCT / overlap-add
      centre carrier. The new MonoLfeData::scaled_spec field carries
      the dequantised + scaled MDCT spectrum; LFE / SSF-frontend / short
      / grouped / Huffman-error cases still leave this None and
      preserve the prior outer-shell-only behaviour.
    • Ac4Decoder::dispatch_acpl_5x_pair now accepts optional
      centre_pcm / ls_pcm / rs_pcm carrier overrides — None
      falls back to the round-36 silence-placeholder; Some(...) threads
      real per-channel PCM through Pseudocode 117's z4 = x2 centre
      passthrough (and the x3in = 2*x3 / x4in = 2*x4 Ls/Rs pre-
      multiplications for ACPL_1 mode). The Cfg0 centre is wired from
      the parsed cfg0_centre_mono.scaled_spec via a new
      imdct_mono_lfe_data_f32 helper.
    • New unit + integration tests:
      • parse_mono_data_non_lfe_walks_sf_data_body — non-LFE long-frame
        ASF-frontend mono walks the trailing body into scaled_spec.
      • parse_mono_data_lfe_skips_body_walk — LFE body decoder remains
        deferred (round-36 behaviour preserved).
      • parse_mono_data_non_lfe_ssf_frontend_skips_body_walk
        SSF-frontend mono skips the ASF body walk.
      • dispatch_acpl_5x_pair_centre_pcm_passthrough_emits_centre_energy
        — supplying a real centre PCM produces non-silent ch2 output.
      • seven_x_pair_dispatch_resolves_same_mode_as_five_x — the 7_X
        dispatch maps SevenXCodecMode::AspxAcpl{1,2} to the same
        Acpl5xPairMode selectors as the 5_X path.
      • imdct_mono_lfe_data_f32_returns_none_when_no_scaled_spec /
        imdct_mono_lfe_data_f32_imdcts_when_scaled_spec_present
        cover the new IMDCT helper.
      • seven_x_aspx_acpl_2_walker_to_synthesis_glue — integration
        test threading parse_7x_audio_data_outer
        acpl_data_1ch_pairrun_acpl_5x_pair_pcm for a non-iframe
        7_X ACPL_2 substream.
  • Round 36 — 5_X ASPX_ACPL_1 / ASPX_ACPL_2 decoder dispatch wiring
    (TS 103 190-1 §5.7.7.6.1, Pseudocode 117):

    • Ac4Decoder::receive_frame now dispatches the §5.7.7.6.1
      Pseudocode 117 channel-pair multichannel synthesis when the 5_X
      walker resolved five_x_mode to AspxAcpl1 or AspxAcpl2 and
      populated the matching acpl_config_1ch_* + acpl_data_1ch_pair
      tools slots. The dispatch:
      • Reads L/R carrier PCM from pcm_per_channel[0]/[1] (already
        filled by the stereo ASF / ASPX decode path), zero-filling on
        absence to keep QMF analysis history consistent.
      • Resolves the active acpl_config_1ch per mode:
        acpl_config_1ch_partial for AspxAcpl1,
        acpl_config_1ch_full for AspxAcpl2 (per Tables 25 + 59).
      • Builds zero-filled centre / Ls / Rs carrier placeholders
        (centre matches the existing ACPL_3 wiring; Ls/Rs only for
        ACPL_1 mode per Acpl5xPairMode mode-vs-surround consistency
        check). The carrier-decode paths gain real signal when
        cfg0_centre_mono and the surround mono carriers acquire
        end-to-end decoders in a future round.
      • Calls acpl_synth::run_acpl_5x_pair_pcm and writes the five
        output channels (L, R, C, Ls, Rs) into pcm_per_channel[0..5],
        growing the slot vector as needed.
      • Skipped when the substream is already an AspxAcpl3 5_X frame
        (the two pipelines are mutually exclusive per Table 97).
    • The dispatch logic is extracted into a private
      Ac4Decoder::dispatch_acpl_5x_pair helper so the path can be
      unit-tested without building a full 5_X TOC + body.
    • Five new unit tests in decoder::tests cover the synthesis
      arithmetic + dispatch behaviour:
      • dispatch_acpl_5x_pair_aspx_acpl_2_emits_five_channels
        ACPL_2 dispatch with ±2000 carrier-PCM tones produces five
        channels with non-zero L / R energy.
      • dispatch_acpl_5x_pair_aspx_acpl_1_emits_five_channels
        ACPL_1 dispatch with zero-filled Ls/Rs surround placeholders
        still emits five-channel output.
      • dispatch_acpl_5x_pair_rejects_unaligned_sample_count
        dispatch is a no-op when sample count isn't a multiple of
        NUM_QMF_SUBBANDS (64).
      • dispatch_acpl_5x_pair_zero_fills_missing_carriers — when L/R
        slots are None, dispatch synthesises silence-grade output
        from zero-filled fallbacks.
      • dispatch_acpl_5x_pair_resolves_partial_for_aspx_acpl_1
        regression check that qmf_band differs between partial
        (1..8) and full (always 0) configs per Table 59.
    • Test count: 535 → 540 (+5).
    • The §5.7.7.6.2 ACPL_3 dispatch (round 34 — Pseudocode 118)
      remains unchanged; the new ACPL_1 / ACPL_2 path takes its place
      when those modes are signalled.
  • Round 35 — ETSI float-table validation suite (TS 103 190-1
    Annex C.1, C.3, C.11 + Annex D.2, D.3):

    • New parse_c_float_arrays() parser in tests/etsi_table_validation.rs
      extracts every const float / const float32 array (1-D and 2-D) from
      the ETSI accompaniment file docs/audio/ac4/ts_10319001v010401p0-tables.c.
      The existing integer parser (round 20) bailed on the float keyword, so
      the five float-typed reference tables had been silently un-validated.
    • New assert_float_table() / assert_complex_float_table() helpers
      compare Rust &[f32] (and &[(f32, f32)]) constants against the
      parsed reference under a 1 ppm absolute / 10 ppm relative epsilon —
      tight enough to catch a single-digit transcription typo in the visible
      decimal prefix, loose enough that f32 literal-rounding noise is
      invisible.
    • Four new tests:
      • etsi_source_parses_floats — sanity-check that the 5 expected float
        arrays are parsed with the right lengths (20, 32, 256, 640, 1024).
      • validate_ssf_float_tablesPOST_GAIN_LUT[20],
        PRED_GAIN_QUANT_TAB[32], RANDOM_NOISE_TABLE[256] against the
        Annex C.1 / C.3 / C.11 reference data.
      • validate_qmf_window — the 640-entry Annex D.3 QWIN prototype
        window against the reference.
      • validate_aspx_noise_table — the Annex D.2 ASPX_NOISE[512][2]
        complex-noise table against the reference (flattened row-major to
        compare &[(f32, f32)] against the parsed flat Vec<f32>).
    • Outcome: all 1,860 published float reference values
      (20 + 32 + 256 + 640 + 1,024) cleared the epsilon test on first run,
      proving the existing transcriptions in ssf_tables.rs, qmf.rs, and
      aspx_noise.rs are byte-correct against the ETSI accompaniment data.
      Future regressions caught at compile-time of the test target rather
      than at decode-time of a fixture.
  • Round 34 — FIXVAR / VARFIX / VARVAR atsg border derivation + SNF injection + 5_X ASPX_ACPL_3 synthesis (TS 103 190-1 §5.7.6.3.3.2 + §5.1.4 + §5.7.7.6.2):

    • New derive_fixvar_atsg() (§5.7.6.3.3.2 Pseudocode 77, FIXVAR arm): builds
      the signal-envelope border vector right-to-left from T - var_bord_right using
      rel_bord_right[] deltas, then reverses to ascending order.
    • New derive_varfix_atsg() (Pseudocode 77, VARFIX arm): builds left-to-right
      from var_bord_left using rel_bord_left[] deltas, appends T.
    • New derive_varvar_atsg() (Pseudocode 77, VARVAR arm): left-side anchors +
      right-side internal anchors + T, totalling num_env + 1 entries.
    • New derive_atsg_borders() dispatcher: routes FIXFIX / FIXVAR / VARFIX /
      VARVAR framing to the matching derivation and computes noise borders
      ([0, T] for 1 noise envelope, [0, mid, T] for 2).
    • Both the TNS path and envelope-adjustment path in decoder.rs now call
      derive_atsg_borders instead of the FIXFIX-only derive_fixfix_atsg, enabling
      A-SPX bandwidth extension for all four interval classes.
    • New inject_snf_noise() in asf_data.rs (§5.1.4 SNF): injects
      shaped noise (gain = 2^((idx * 1.5 - 84) / 4)) into zero-energy MDCT
      bins using a 16-bit LCG (multiplier 69069, addend 1). Previously the
      parse_asf_snf_data() result was discarded; now it is consumed by the
      long-mono ASF decode path.
    • 5_X ASPX_ACPL_3 synthesis wired into Ac4Decoder::receive_frame:
      Ac4Decoder gains two new persistent fields (acpl_5x_pair_state,
      acpl_5x_mch_state); when the walker populates acpl_config_2ch +
      acpl_data_2ch + stereo carrier spectra, run_acpl_5x_mch_pcm
      (Pseudocode 118) runs and fills pcm_per_channel[0..5] with L/R/C/Ls/Rs.
    • Unit tests: derive_fixvar_atsg (2 cases + reject), derive_varfix_atsg
      (2 cases + reject), derive_varvar_atsg (2 cases), derive_atsg_borders
      dispatch (FIXFIX + FIXVAR), inject_snf_noise (fill, gain formula, LCG).

Changed

  • register entry point unified on RuntimeContext (task #502).
    The legacy pub fn register(reg: &mut CodecRegistry) is renamed to
    register_codecs and a new pub fn register(ctx: &mut oxideav_core::RuntimeContext) calls it internally. Breaking change
    for direct callers passing a CodecRegistry; switch to either the
    new RuntimeContext entry or the explicit register_codecs name.