oxideav-ac4
Pure-Rust Dolby AC-4 audio decoder foundation — sync / TOC / presentation
/ substream parsing, plus a stub decode path that emits silence at the
correct channel count and sample rate so container fixtures can round-trip
without crashing. Zero C dependencies, no FFI, no *-sys crates.
Part of the oxideav framework but usable standalone.
Status: Foundation. AC-4 is a complex codec. This crate parses the bitstream framing, the table of contents (
ac4_toc()), presentations and substream descriptors per ETSI TS 103 190-1 V1.4.1, and exposes a decoder that emits PCM with the right shape. Mono ASF, stereo CPE (split + joint MDCT), full A-SPX front-end, A-CPL channel-pair synthesis (ASPX_ACPL_1 / ASPX_ACPL_2), DRC + DE + outer metadata walker are all implemented. Round 20 unblocks the ETSI Huffman-table audit (60 codebooks validated byte-for-byte against the canonical ETSI accompaniment file intests/etsi_table_validation.rs) and wires the 5.X channel-element walker family's Cfg0 / Cfg1 / Cfg2 outer shells plus a Table-21-correctsf_info_lfe()parser. Round 21 lands the §5.7.7.6.2 ASPX_ACPL_3 transform-matrix synthesis math (Pseudocodes 118/119 —Transform(),ACplModule2(),ACplModule3()and the full 5-channel pipelinerun_pseudocode_118_5x()). Round 22 lands §5.7.7.6.1 ASPX_ACPL_1 / ASPX_ACPL_2 multichannel wrappers (Pseudocode 117 —run_pseudocode_117_5x(): two parallel ACplModule's with D0/D1 decorrelators) plus the 5_X-walker glue: PCM-level helpersrun_acpl_5x_pair_pcm()(ASPX_ACPL_1/2) andrun_acpl_5x_mch_pcm()(ASPX_ACPL_3) consume the parsedacpl_config_*+acpl_data_*to produce 5-channel L/R/C/Ls/Rs PCM end-to-end via QMF analysis → A-CPL → QMF synthesis. Round 23 wires the per-channelsf_data(ASF)Huffman bodies for the multichannel layouts (Tables 26 / 27 / 28 / 29):parse_two_channel_data/parse_three_channel_data/parse_four_channel_data/parse_five_channel_datanow also walk the trailing 2 / 3 / 4 / 5sf_data(ASF)calls throughdecode_mch_sf_data_channels()and deposit the per-channel scaled MDCT spectra on each*ChannelData::scaled_spec_per_channelfor the long-frame, single-window-group case. The Huffman codebook IDs reused areHCB_1..HCB_11(spectral lines),HCB_SCALEFAC(scale-factor DPCM) andHCB_SNF(spectral noise fill) — Annex A.1 shares the codebooks across mono / stereo / multichannel; there is no separate "MCH" codebook set. Round 24 closes the two r23 follow-ups: (1) the grouped / short-frame multichannelsf_data(ASF)walker (num_window_groups > 1) is now driven bydecode_asf_grouped_mono_body_with_max_sfb()— each per-channel spectrum is the concatenation ofnum_window_groupsindependent(section + spectral + scalefac + snf)chains, group-major; (2) the ASPX_ACPL_3 inner body walker is now wired inparse_5x_audio_data_outer— on an I-frame the walker chainsstereo_data() + aspx_data_2ch() + acpl_data_2ch()and the parsedtools.acpl_data_2chflows straight into the §5.7.7.6.2 Pseudocode-118 5_X synthesis pipeline. The Table-52aspx_data_2ch()body parser was factored out of the stereo CPE ASPX path into a sharedparse_aspx_data_2ch_body()helper — both the stereo CPE mode and the 5_X ASPX_ACPL_3 mode use the same parser. Round 25 wires the ASPX_ACPL_1 / ASPX_ACPL_2 inner body walker inparse_5x_audio_data_outerper §4.2.6.6 Table 25 (case ASPX_ACPL_1: case ASPX_ACPL_2:): a newparse_aspx_acpl_1_2_inner_body()helper walkstwo_channel_data() / three_channel_data()(selected by the 1-bitcoding_config), the ASPX_ACPL_1-only joint-MDCT residual layer (max_sfb_master + 2x chparam_info + 2x sf_data(ASF)over the dominant transform length signalled by the upstream channel data —n_side_bitsis derived per the §4.2.6.6 NOTE), the optional Cfg0 trailermono_data(0), thenaspx_data_2ch()+aspx_data_1ch()and finally the two parallelacpl_data_1ch()calls per Pseudocode 117. The pair lands intools.acpl_data_1ch_pair[0/1](D0 / D1 ACplModule). The walker is try-and-bail: any inner Huffman / parse miss leaves the already-populatedtools.*slots intact and returns silently. Round 27 lands the 7_X channel-element walker (parse_7x_audio_data_outer) per §4.2.6.14 Table 33 — immersive 7.0 and 7.1 streams now parse end-to-end. The 7.X walker mirrors the 5_X SIMPLE/ASPX path'scoding_configselector but has its own quirks: 2-bit7_X_codec_mode(no ASPX_ACPL_3 in 7.X),companding_control(5)only on ASPX_ACPL_{1,2}, the centre/back monos move out of the coding_config switch into a single trailingmono_data(0)gated oncoding_config in {0, 2}, and a SIMPLE/ASPX-only additional-channel block (b_use_sap_add_ch + optional chparam_info×2 + two_channel_data) carries the front-extension / back-surround pair beyond the 5.X core.walk_ac4_substreamnow dispatcheschannels == 7/8(7.0/7.1) into the new walker. Round 28 lands the mono / stereo short-framesf_data(ASF)walker per ETSI TS 103 190-1 §4.2.8.3-6 Tables 39-42: new spec-correct_groupedpayload parsers inasf_data.rs(each with its own outerfor (g = 0; g < num_window_groups; g++)loop, a single 8-bitreference_scale_factorat the head ofasf_scalefac_data()withfirst_scf_foundcarrying across groups, and a single 1-bitb_snf_data_existsgate at the head ofasf_snf_data()), plusderive_per_group()helpers that resolve per-group(transf_length_idx, transform_length, max_sfb)from(ti, psy)per Pseudocodes 2 / 3 / 5 (handling theb_different_framinghalf-frame split). New body decodersdecode_asf_grouped_mono_body[_with_max_sfb]()anddecode_asf_grouped_stereo_joint_body()(shared section, per-groupms_used[g][sfb], inverse M/S) are wired into all four mono / stereo call sites:parse_mono_audio_data_outer,parse_aspx_acpl2_mdct_body,parse_aspx_acpl1_mdct_body(joint + split) andparse_stereo_data_body(joint + split). Real Dolby AC-4 mono / stereo streams using short-window sub-frames now decode end-to-end without bailing at the previousnum_window_groups != 1guard. Round 29 lands the full §5.2.8 SSF arithmetic decoder + Annex C scalar inventory + 37 prediction-coefficient matrices — 705-entryCDF_TABLE,PREDICTOR_GAIN_CDF_LUT,ENVELOPE_CDF_LUT,DITHER_TABLE/RANDOM_NOISE_TABLE,STEP_SIZES_Q4_15,AC_COEFF_MAX_INDEX, the four C.14 dB↔linear LUTs, plusAcState(init/decode_target/decode_symbol_ext_cdf/decode_symbol_calc_cdf/decode_finishper Pseudocodes 41-47), theIdx2Reconstruction + CdfEstcomputed-CDF path (Pseudocodes 51-53), envelope / predictor-gain / coefficient convenience entry points (Pseudocodes 48-50), and theSsfRandGenStatedither + noise RNG (Pseudocodes 54-57). Round 30 lands the SSF bitstream walker (ssf::parse_ssf_data/parse_ssf_granule/parse_ssf_st_data/parse_ssf_ac_dataper Tables 43-46), the Annex C.1 SSF-bandwidths matrix (SSF_BANDWIDTHS, 19 bands × 8 block-length columns),SsfBinLayout::build()(Pseudocode 7 —start_bin[]/end_bin[]/num_bins),SsfFrameConfig(Tables 112-113), and theSsfChannelStatecarrying RNG /prev_pred_lag_idx/last_num_bands/env_prev[]across granules. Wired intowalk_ac4_substreamfor mono SIMPLE/ASPX, split-MDCT stereo, and the ASPX_ACPL_1 split residual layer —spec_frontend == SSFno longer falls through silently. Round 31 lands the §5.2.3-5.2.7 SSF PCM synthesis chain in a newssf_synthmodule: envelope decoder + predictor + helpers + lossless decode + inverse-quant + subband predictor + inverse-flattening (Pseudocodes 4a / 4b / 4c / 4d / 4e / 26 / 31 / 32 / 33 / 34 / 35 / 36 / 37 / 38) plus the C-matrix reconstruction (Pseudocode 39) for all 37tab_idxvalues.synthesize_ssf_data()threadsenv_prev[]between granules.Ac4Decodernow carries a per-channelVec<SsfSynthState>and consumestools.ssf_data_primary/tools.ssf_data_secondaryafter the ASF/A-CPL pipeline: each granule'snum_blocks * n_mdctspectrum is split per-block and IMDCT'd through the existing KBD overlap-add path. SSF substreams now emit real PCM in place of silence. §5.2.5.2.2 Heuristic Scaling (Pseudocodes 27 / 28 / 29 / 30) is deferred — the spec'sf_rfu == 0short-circuit covers any block with the predictor disabled, which the current synth supports. Round 32 closes the SHORT_STRIDE P-frame correctness gap by addingenv_prev: Vec<i32>toSsfSynthState:synthesize_granule()latches the resolved envelope (post-decode_envelopeδ-chain) at the end of each granule so that a SHORT_STRIDE P-granule with no caller-suppliedenv_prev[]interpolates against the previous frame's envelope rather than a zero fallback (§5.2.3.0 Note 2). The walker side gets a parallel hoist:Ac4Decodernow owns aVec<SsfChannelState>(ssf_walker_state) and a newwalk_ac4_substream_stateful()threads it through the SSF body parses so dither / noise RNGs (Pseudocodes 54-57) andprev_pred_lag_idx/last_num_bandspersist across frames — pre-r32 the walker built a fresh state per frame and dropped it. Round 33 lands §5.2.5.2.2 Heuristic Scaling (Pseudocodes 27/28/29/30) — the predictor-enabled spectrum-decoding branch the spec'sf_rfu == 0short-circuit previously skipped. Newmap_db_to_lin_q10()/map_lin_to_db_q10()Q.10 fixed-point converters use the Annex C.14 LUTs;heuristic_scaling()runs the full Pseudocode 28 chain (dynamic-range compression ofenv_in[], sorted-descendingMap_dB_to_Lin,iRfu²-weighted reverse water-filling,Map_Lin_to_dB-driven per-band weight);apply_heuristic_scaling()wraps it with the Pseudocode 27env_in = 3 * env_allocpre-multiply, LF-boost, and(env_alloc_mod, f_gain_q)post-processing.synthesize_granule()dispatches the §5.2.5.2.0 selector — whenf_rfu > 0 && !variance_preservingthe heuristic-scaling branch fires andinverse_heuristic_scale()consumes the resultingf_gain_q[]instead of the all-1 stub;variance_preservingblocks correctly skip the inverse-scale call per §5.2.5.2.0 step 5. Round 34 lands FIXVAR / VARFIX / VARVAR atsg border derivation (§5.7.6.3.3.2 Pseudocode 77) — newderive_fixvar_atsg(),derive_varfix_atsg(),derive_varvar_atsg()and a unifiedderive_atsg_borders()dispatcher cover all fouraspx_int_classvalues; the decoder's TNS and envelope-adjustment paths now route throughderive_atsg_bordersinstead of the FIXFIX-only path, enabling A-SPX bandwidth extension for FIXVAR / VARFIX / VARVAR substreams. §5.1.4 SNF injection:inject_snf_noise()fills zero-energy MDCT bins using a 16-bit LCG (multiplier 69069, addend 1) and gain formula2^((idx×1.5−84)/4); the long-mono ASF decode path now consumesparse_asf_snf_data()output instead of discarding it. 5_X ASPX_ACPL_3 wired inAc4Decoder: two new persistent state fields (acpl_5x_mch_state/acpl_5x_pair_state) are added; whenacpl_config_2ch+acpl_data_2ch+ stereo carrier spectra are present,run_acpl_5x_mch_pcm()(Pseudocode 118) fires and fillspcm_per_channel[0..5]with L/R/C/Ls/Rs surround PCM. Round 35 lands the §4.2.4.4 EMDF payloads substream parser (Table 18) plus the §4.2.14.14emdf_payload_config()(Table 79) in a newemdfmodule —parse_emdf_payloads_substream()walks the while-loop until theemdf_payload_id == 0terminator, handles theid == 31 → variable_bits(5)extension, decodes the fullEmdfPayloadConfig(sample-offset / duration / group-id / codecdata / discard / frame-aligned + create-/remove-duplicate / priority / proc_allowed gates per the Table 79 conditional tree), and captures each payload'semdf_payload_byte[]verbatim. Defensive caps (MAX_EMDF_PAYLOADS = 64,MAX_EMDF_PAYLOAD_BYTES = 65 536) bound malformed input. The outermetadata::parse_metadatawalker now consumes the substream whenb_emdf_payloads_substream == 1and surfaces it throughMetadata::emdf_payloads_substreaminstead of erroring out with "not yet implemented" — real-bitstream metadata can now fully round-trip through the walker. Round 35 also lands the §5.7.9.3.3 PCM gain application path:drc::drc_raw_to_linear()maps a 7-bitdrc_gain[ch][sf][band]value to its linear multiplier via2^((raw-64)/6),dialnorm_correction_linear()resolves the2^((Lout-Lin)/6)dialnorm correction, anddrc::apply_drc_gains_to_pcm()applies a parsedDrcGains(per channel-group, per subframe — multi-band averaged in the linear domain) to a planar&mut [Vec<f32>]PCM buffer with aDrcChannelMap(helpers for the[L, R, C, LFE?, Ls, Rs]5_X layout and the wideband single-group mono/stereo case). DE walker hardened with three new edge-case tests covering EOF on truncation, non-I-frame withoutprev_config, and thenr_channels == 0degenerate case. Round 51 lands stereo SIMPLE/ASF split-MDCT (Path A: 2× SCE) encoding per ETSI TS 103 190-1 §5.3 + §4.2.6.3 —Ac4ImsEncoder::encode_frame_pcm_stereoaccepts L+R PCM frames, runs the existing forward MDCT + scalefactor + DP-section + HCB1..11 codebook-selection + SNF emission pipeline independently per channel, and emits ab_enable_mdct_stereo_proc == 0stereo CPE body the decoder reconstructs at ≥24.8 dB spectral SNR for both 440 Hz L+R (matched) and 440 Hz L + 660 Hz R (independent) tone fixtures. Round 52 lands the matching joint M/S CPE (Path B,b_enable_mdct_stereo_proc == 1) encoder per §5.3 + §4.2.6.3
- §7.5:
encode_frame_pcm_stereonow dispatches between Path A and Path B based on an energy-weighted per-SFB correlation rising above the 0.7 threshold. Path B emits one sharedasf_section_data- two
asf_spectral_data(M/S or L/R per band based on bit-cost comparison) + sharedasf_scalefac_data+ per-active-sfbms_usedflags + sharedasf_snf_data. A frame-level "matched-channels" gate (S/M energy ratio < 0.15) bumps the M-channel q_target up to 16 to spend the bits saved on the silent / near-silent S residual on finer M quantisation. End-to-end SNR on the 440 Hz L=R matched fixture rises from round-51's 24.8 dB to 34.5 dB; the half- correlated 440 Hz amplitude-imbalance fixture clears ≥ 26 dB; independent 440 L + 660 R correctly routes via Path A and preserves the round-51 SNR floor. Round 74 lands the first multichannel forward-analysis encoder: a 5.0 SIMPLE/Cfg3Five path (5 SCE, no LFE, no joint coding) per §4.2.6.6 Table 25 rowcase SIMPLE: coding_config == 3+ §4.2.7.5 Table 29 (five_channel_data()). NewAc4ImsEncoder::with_5_0()flips the TOC channel_mode prefix to0b1101(4 b — Table 85 channel_mode 3), andencode_frame_pcm_5_0(&[L, R, C, Ls, Rs])runs the round-50 forward pipeline (KBD-MDCT + DP-optimal sectioning + HCB1..11- SNF) independently per channel into a shared
sf_info(ASF, 0, 0)header followed byfive_channel_info()(identity SAP:chel_matsel = 0+ 5xchparam_infowithsap_mode = 0) and 5xsf_data(ASF)bodies. Decoder's existingdispatch_5x_cfg3_simple_aspx(round 39) consumes the body and emits 5-channel interleaved S16 PCM. End-to-end per- channel spectral SNR ≥ 20 dB 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), matching the round-51 stereo Path A SNR floor. Round 80 extends the multichannel encoder to 5.1 (Cfg3Five + LFE) per §4.2.6.6 Table 25 (if (b_has_lfe) mono_data(1);) + §4.2.8 (sf_info_lfe()Table 35 / Table 106 column 4n_msfbl_bits):Ac4ImsEncoder::encode_frame_pcm_5_1(&[L, R, C, Ls, Rs, LFE])flips the TOC channel_mode prefix to0b1110(4 b — Table 85 channel_mode 4) andbuild_5_1_simple_asf_body_from_pcm_spectraprepends an LFEmono_data(1)element (b_long_frame = 1+sf_info_lfe()withmax_sfb_lfecapped ton_msfbl_bits = 3→ 7 sfb / ≈350 Hz at tl = 1920) before the Cfg3Fivefive_channel_data()body. The decoder side gains a matching LFE PCM render inAc4Decoder::receive_frame: whenchannels == 6(5.1) orchannels == 8(7.1) andtools.lfe_mono_data.scaled_specis present, the LFE spectrum is IMDCT'd into the trailing PCM slot (slot 5 for 5.1, slot 7 for 7.1). Non-LFE per-channel SNR matches the 5.0 numbers (L=24.5 / R=24.8 / C=25.0 / Ls=23.4 / Rs=27.4 dB ≥ 20 dB floor); a 60 Hz LFE tone round-trips to a non-silent reconstructed LFE channel. Round 91 extends the multichannel encoder to 7.1 (3/4/0.1) SIMPLE/Cfg3Five + LFE per §4.2.6.14 Table 33 + §4.2.7.4 Table 26 (additional-channeltwo_channel_data()) + Table 88 channel_mode 6 (0b1111001, 7 b):Ac4ImsEncoder::with_7_1()+encode_frame_pcm_7_1(&[L, R, C, Ls, Rs, Lb, Rb, LFE])emit a 7_X SIMPLE/Cfg3Five body whose innerfive_channel_data()reuses the round-80 5.1 forward pipeline, followed byb_use_sap_add_ch = 0identity-SAP +two_channel_data()for the immersive Lb/Rb pair. The decoder side gains the inner 5-channel core render for the 7_X SIMPLE/Cfg3Five path (it previously parsed but never IMDCT'd slots 0..4 — only the round-39 additional-pair slots 5/6 and the round-80 LFE slot 7 were touched); slots 0..4 now route throughdispatch_5x_cfg3_simple_aspx(the inner SCE shape is identical to the 5_X Cfg3Five case). Per-channel 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. Round 95 lands the 5_X SIMPLE/ASPX_ACPL_3 multichannel encoder path per §4.2.6.6 Table 25 rowcase ASPX_ACPL_3:— symmetric counterpart to the round-34 decoder ACPL_3 walker (5a58f6a).Ac4ImsEncoder::encode_frame_pcm_5_0_acpl3(&[L, R, C])andencode_frame_pcm_5_1_acpl3(&[L, R, C, LFE])emit IMS v2 frames with5_X_codec_mode = 4(ASPX_ACPL_3). The newencoder_acpl3module ships bit-exact emitters foraspx_config()(Table 50, 15 b),acpl_config_2ch()(Table 60, 4 b),companding_control(2)(Table 49, 2 b), plus minimum-bit-cost zero-delta Huffman writers covering 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) —pick_zero_delta_cwpicks the entry atindex == cb_off(zero delta for DF/DT) andpick_min_len_cwpicks the smallest-length entry (used for F0 seeds). The body layout is5_X_codec_mode = 4 (3 b)+ I-frameaspx_config() + acpl_config_2ch()+ optional LFEmono_data(b_lfe = 1)+companding_control(2)+stereo_data()(split-MDCT path, two carrier channels) +aspx_data_2ch()(FIXFIX num_env=1, balance=1, all-FREQ deltas) +acpl_data_2ch()(1 param set, 7 param bands, 11 EC streams withdiff_type = 0and zero-delta DF). Decoder round-trip: 5.0 ACPL_3 → 5-channel S16 interleaved PCM (1920 samples × 5 ch × 2 bytes); 5.1 ACPL_3 → 6-channel S16 (with LFE slot 5). The decoder walks the full Table 25 body and resolvesfive_x_mode == AspxAcpl3acpl_config_2ch.is_some() && acpl_data_2ch.is_some(). The 5-channel[L, R, C, Ls, Rs]synthesis runs viaacpl_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 (non-silent in the general case). Total tests 691 (was 680). Real per-band(alpha, beta, gamma)parameter extraction (replacing the zero-delta scaffold), real ASPX envelope coding, and matching encoder paths for5_X_codec_mode in {ASPX_ACPL_1, ASPX_ACPL_2}(Pseudocode 117) remain deferred. The 7_X paths inherit the sameaspx_data/acpl_datashape as 5_X so they're queued behind the 5_X work continuing to harden. Round 100 lands the 5_X SIMPLE/ASPX_ACPL_2 multichannel encoder path per §4.2.6.6 Table 25 rowcase ASPX_ACPL_2:— the symmetric counterpart to the round-25 decoderparse_aspx_acpl_1_2_inner_bodywalker (Pseudocode 117).Ac4ImsEncoder::encode_frame_pcm_5_0_acpl2(&[L, R, C])emits an IMS v2 frame with5_X_codec_mode = ASPX_ACPL_2 (3)whose body isaspx_config() + acpl_config_1ch(FULL) + companding_control(3) + coding_config = 0 + two_channel_data() (L/R carriers) + mono_data(0) (centre) + aspx_data_2ch() + aspx_data_1ch() + two acpl_data_1ch()parameter sets. 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. Newencoder_acpl3emitters:write_acpl_config_1ch_full(Table 59, 3 b),write_two_channel_data(Table 26 — sharedsf_info(ASF)+ identity-SAPchparam_info+ 2×sf_data),write_mono_data_centre(Table 21 non-LFE),write_aspx_data_1ch_minimal(Table 51 FIXFIX num_env=1) andwrite_acpl_data_1ch_minimal(Table 61). The 1ch ASPX SIGNAL band count usesnum_sbg_sig_highresto matchparse_aspx_ec_data's empty-freq_resfallback. Decoder round-trip: 5.0 ACPL_2 → 5-channel S16 interleaved PCM; the decoder resolvesfive_x_mode == AspxAcpl2, walksacpl_config_1ch_full,two_channel_data, the Cfg0 centremono_data(0), and bothacpl_data_1ch_pairentries, then synthesises[L, R, C, Ls, Rs]viaacpl_synth::run_acpl_5x_pair_pcm. Total tests 700 (was 691). The ASPX_ACPL_1 encoder path (joint-MDCT residual + PARTIAL-modeacpl_config_1chwithacpl_qmf_band), real(alpha, beta)parameter extraction, and the 7_X ASPX_ACPL_{1,2} encoder paths remain deferred. Round 103 lands the 5_X SIMPLE/ASPX_ACPL_1 multichannel encoder path per §4.2.6.6 Table 25 rowcase ASPX_ACPL_1:— the round-100 follow-up and the symmetric counterpart to the decoder's round-25parse_aspx_acpl_1_2_inner_bodyASPX_ACPL_1 branch (Pseudocode 117).Ac4ImsEncoder::encode_frame_pcm_5_0_acpl1(&[L, R, C, Ls, Rs])emits an IMS v2 frame with5_X_codec_mode = ASPX_ACPL_1 (2). The body differs from the ACPL_2 path in two structural places: (1)acpl_config_1chis PARTIAL —write_acpl_config_1ch_partial(Table 59, 6 b: id + quant_mode +acpl_qmf_band_minus1), so theacpl_data_1ch()start_band resolves fromqmf_bandviasb_to_pb; (2) the body carries an explicit joint-MDCT residual layer —write_acpl_1_residual_layeremitsmax_sfb_master(n_side bits) + 2× identity-SAPchparam_info+ 2×sf_data(ASF)for the Ls/Rs surround pair (sSMP,3 / sSMP,4 per Table 181), so the encoder takes a full 5-channel input instead of reconstructing Ls/Rs from the L/R carriers. Newbuild_5_x_acpl1_body_from_pcm_spectralays out5_X_codec_mode = 2 + aspx_config() + acpl_config_1ch(PARTIAL) + companding_control(3) + coding_config = 0 + two_channel_data() + residual-layer + mono_data(0) + aspx_data_2ch() + aspx_data_1ch() + 2× acpl_data_1ch(). Decoder round-trip: 5.0 ACPL_1 → 5-channel S16 interleaved PCM; the decoder resolvesfive_x_mode == AspxAcpl1, the PARTIAL config, the persisted residual pair, the Cfg0 centre, and bothacpl_data_1ch_pairentries, then synthesises[L, R, C, Ls, Rs]viaacpl_synth::run_acpl_5x_pair_pcm. Total tests 708 (was 700). Real per-band(alpha, beta)extraction, real ASPX envelope coding, real Table-181 SAP-derived residual content (the residual sf_data currently codes the raw Ls/Rs spectra), and the 7_X ASPX_ACPL_{1,2} encoder paths remain deferred. Round 107 lands the first of those deferred 7_X encoder paths: the 7.0 SIMPLE/ASPX_ACPL_2 multichannel encoder per §4.2.6.14 Table 33 rowcase ASPX_ACPL_2:— the 7_X (immersive) counterpart to the round-100 5_X ASPX_ACPL_2 path and the encoder side of the decoder's round-27parse_7x_audio_data_outerASPX_ACPL_2 branch.Ac4ImsEncoder::encode_frame_pcm_7_0_acpl2(&[L, R, C, Ls, Rs, Lb, Rb])emits an IMS v2 frame with7_X_codec_mode = ASPX_ACPL_2 (3)and channel_mode prefix0b1111000(7 b — Table 85 channel_mode 5, 7.0 (3/4/0)). The newencoder_acpl3::build_7_x_acpl2_body_from_pcm_spectrareuses the same 1ch ACPL / ASPX emitters as the 5_X ACPL_2 path but lays out the 7_X channel element's distinct framing: 2-bit7_X_codec_mode(vs the 5_X 3-bit field),companding_control(5)(sync-on 2-bit wire shape), 2-bitcoding_config = 0(Cfg0),b_2ch_mode + two_channel_data() (L/R) + two_channel_data() (Ls/Rs), a trailing centremono_data(0)moved out of the coding_config switch, and anaspx_data_2ch() + aspx_data_2ch() + aspx_data_1ch()envelope trailer before the twoacpl_data_1ch()parameter sets (Pseudocode 117 D0/D1). The ASPX_ACPL_1-only joint-MDCT residual layer and the SIMPLE/ASPX additional-channel block are skipped for ACPL_2. Decoder round-trip: 7.0 ACPL_2 → 7-channel S16 interleaved PCM (1920 samples × 7 ch × 2 bytes); the decoder resolvesseven_x_mode == AspxAcpl2, bothtwo_channel_datapairs, the Cfg0 centre, and bothacpl_data_1ch_pairentries, then synthesises[L, R, C, Ls, Rs](slots 0..4) viaacpl_synth::run_acpl_5x_pair_pcm(the back pair Lb/Rb stays silent per the Table 202 ACPL_2 mapping). Total tests 714 (was 708). The 7.1 (LFE) ASPX_ACPL_2 path, the 7_X ASPX_ACPL_1 path (PARTIAL config + joint-MDCT residual), real per-band(alpha, beta)extraction, real ASPX envelope coding, and back-pair Lb/Rb carriage remain deferred. Round 114 closes the first of those deferred items — the 7.1 (3/4/0.1) SIMPLE/ASPX_ACPL_2 multichannel encoder path per §4.2.6.14 Table 33 rowcase ASPX_ACPL_2:withb_has_lfe = 1+ §4.2.6.5 Table 21 (mono_data(b_lfe)) + §4.2.8 Table 35 (sf_info_lfe()).Ac4ImsEncoder::encode_frame_pcm_7_1_acpl2(&[L, R, C, Ls, Rs, Lb, Rb, LFE])emits the round-107 7.0 ASPX_ACPL_2 body plus a leadingmono_data(b_lfe = 1)element between the I-frame config block andcompanding_control(5)— exactly where the decoder'sparse_7x_audio_data_outer(b_has_lfe = true)readsif (b_has_lfe) mono_data(1);. The channel_mode prefix is forced to0b1111001(7 b — Table 88 channel_mode 6) so the decoder dispatcheschannels == 8.build_7_x_acpl2_body_from_pcm_spectragainedmax_sfb_lfe: Option<u32>+coeffs_lfe: Option<&[f32]>and reuses the shared round-80write_lfe_mono_dataemitter (max_sfb_lfecapped ton_msfbl_bits = 3→ 7 sfb attl = 1920). Decoder round-trip: 7.1 ACPL_2 → 8-channel S16 interleaved PCM (1920 samples × 8 ch × 2 bytes); the LFE spectrum IMDCT's into slot 7 via the round-80 LFE render, the[L, R, C, Ls, Rs]slots 0..4 synthesis is unchanged, and the back pair Lb/Rb (slots 5/6) stays silent per the Table 202 ACPL_2 mapping. A 60 Hz LFE tone round-trips to a non-silent reconstructed LFE channel. Total tests 721 (was 714). The 7_X ASPX_ACPL_1 path (PARTIAL config + joint-MDCT residual), real per-band(alpha, beta)extraction, real ASPX envelope coding, and back-pair Lb/Rb carriage remain deferred. Round 118 closes the first of those — the 7.0 / 7.1 (3/4/0(.1)) SIMPLE/ASPX_ACPL_1 multichannel encoder path per §4.2.6.14 Table 33 rowcase ASPX_ACPL_1:(the 7_X analogue of the round-103 5_X ACPL_1 path).Ac4ImsEncoder::encode_frame_pcm_7_0_acpl1(&[L, R, C, Ls, Rs, Lb, Rb])andencode_frame_pcm_7_1_acpl1(&[.., LFE])emit IMS v2 frames with7_X_codec_mode = ASPX_ACPL_1 (2). Newencoder_acpl3::build_7_x_acpl1_body_from_pcm_spectrais the round-107/114 7_X ACPL_2 body with the three ACPL_1 differences:7_X_codec_mode = 2(not 3),acpl_config_1chPARTIAL (write_acpl_config_1ch_partial, carriesacpl_qmf_band_minus1→acpl_data_1ch()start_band viasb_to_pb), and an explicit joint-MDCT residual layer (write_acpl_1_residual_layer:max_sfb_master + 2× chparam_info + 2× sf_data(ASF)for the Ls/Rs surround pair sSMP,3 / sSMP,4 per Table
- after the two
two_channel_data()pairs and before the trailing Cfg0 centremono_data(0). The 7.1 form prepends the round-80write_lfe_mono_dataLFE element. Decoder round-trip: 7.0 ACPL_1 → 7-channel S16 PCM; 7.1 ACPL_1 → 8-channel S16 (LFE IMDCT'd into slot 7);[L, R, C, Ls, Rs]synthesise viaacpl_synth::run_acpl_5x_pair_pcm, Lb/Rb (slots 5/6) silent per Table 202. Total tests 729 (was 721). Real per-band(alpha, beta)extraction, real ASPX envelope coding, real Table-181 SAP-derived residual content, and back-pair Lb/Rb carriage remain deferred. Round 125 lands the 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-channeltwo_channel_data()) — the non-LFE immersive counterpart of round-91's 7.1 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 to0b1111000(7 b — Table 85 channel_mode 5, 7.0 (3/4/0) → 7 channels), andencode_frame_pcm_7_0(&[L, R, C, Ls, Rs, Lb, Rb])(+..._with_max_sfb(.., max_sfb, max_sfb_add)) emits a SIMPLE/Cfg3Five 7_X channel-element body whose innerfive_channel_data()reuses the round-80 5.1 forward pipeline for the L/R/C/Ls/Rs front/surround pair and whose trailing identity-SAPtwo_channel_data()(b_use_sap_add_ch = 0) carries the immersive Lb/Rb pair. The body is structurally the round-91 7.1 body with the leadingmono_data(b_lfe = 1)element omitted (the walker'sif (b_has_lfe) mono_data(1);branch is gated off for channel_mode 5). Newencoder_asf::build_7_0_simple_asf_body_from_pcm_spectraemits the body bytes; decoder round-trip: 7.0 → 7-channel S16 interleaved PCM (1920 samples × 7 ch × 2 bytes). The 7.0 walker resolvesseven_x_mode == Simple,seven_x_b_has_lfe == false,five_channel_datapopulated, identity-SAP additional-channel pair populated (slots 5/6 = Lb/Rb routed viadispatch_7x_additional_channel_pair). 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. Total tests 737 (was 729). Round 128 lands the first real per-parameter-band α extraction in the ACPL_1 5.0 encoder 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 (β / β3 / γ stay at the round-95 / 100 / 103 scaffold; β3 / γ only fire in ASPX_ACPL_3 anyway). With β = 0 the surround reconstruction isLs_recon = 0.5/√2 · L · (1 − α); solving for α per parameter band gives the closed formα = 1 − 2·√2 · ⟨L, Ls⟩ / ⟨L, L⟩. Newencoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra_real_alpha
- helper chain (
compute_per_band_correlationsmapping MDCT bins → QMF subbands → A-CPL parameter bands via §5.7.7.2 Table 197,analytic_alpha_per_band+quantise_alphaagainst Tables 203 / 205, thenwrite_acpl_alpha_f0_value/write_acpl_alpha_df_valueemit the ALPHA F0 + DF codewords per Tables A.35 / A.34); newAc4ImsEncoder::encode_frame_pcm_5_0_acpl1_real_alphaentry point alongside the round-103 zero-delta variant. The on-wire body structure is unchanged — decoder resolvesFiveXCodecMode::AspxAcpl1, bothacpl_data_1ch_pair[0/1]populated, joint-MDCT residual layer walked,[L, R, C, Ls, Rs]synthesised viaacpl_synth::run_acpl_5x_pair_pcm. Total tests 743 (was 737). Round 132 adds the first real per-parameter-band β extraction in the ACPL_1 5.0 encoder per ETSI TS 103 190-1 §5.7.7.5 Pseudocode 116 + §5.7.7.6.1 Pseudocode 117 — replaces the round-95/100/103/128 zero-β scaffold for the ACPL_1 path. With the decorrelator outputy⊥x0andE[y²] ≈ E[x0²], the surround energy balance isE[Ls²] = 0.5·E[x0²]·((1−α)² + β²), so the per-band β magnitude isβ = √max(0, 2·E[Ls²]/E[x0²] − (1−α_dq)²). Newencoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra_real_alpha_beta- helper chain (
compute_per_band_energies,analytic_beta_per_band,quantise_beta_magnitudeagainst Tables 204/206,write_acpl_beta_f0_value/write_acpl_beta_df_valueper Tables A.40/A.41) and the newAc4ImsEncoder::encode_frame_pcm_5_0_acpl1_real_alpha_betaentry point. The on-wire body structure is unchanged; the β coding contract round-trips byte-exact throughacpl::parse_acpl_data_1ch. Total tests 755 (was 743). Real β extraction for the 7_X / ACPL_2 / ACPL_3 paths, real γ extraction, and the round-128 ALPHA-writer negative-alpha_qdesync fix remain deferred. Round 135 extends the real per-band α + β extraction to the 7_X (7.0 immersive) ASPX_ACPL_1 path — the round-132 followup. Newencoder_acpl3::build_7_x_acpl1_body_from_pcm_spectra_real_alpha_beta(the real-α+β upgrade of the round-118 zero-delta 7_X builder, reusing theextract_alpha_q_per_band/extract_beta_q_per_bandprimitives) and theAc4ImsEncoder::encode_frame_pcm_7_0_acpl1_real_alpha_betaentry point; both trailingacpl_data_1ch()sets now carry real α + β. The on-wire body structure is unchanged — decoder resolvesSevenXCodecMode::AspxAcpl1(b_has_lfe = false), bothacpl_data_1ch_pair[0/1]populated, joint-MDCT residual layer walked. Total tests 760 (was 755). Round 139 extends the real per-band α + β extraction to the 7.1-with-LFE (3/4/0.1) ASPX_ACPL_1 path — the round-135 LFE follow-up. NewAc4ImsEncoder::encode_frame_pcm_7_1_acpl1_real_alpha_beta(and the_with_max_sfbform) reuses the round-135build_7_x_acpl1_body_from_pcm_spectra_real_alpha_betabuilder with the LFEcoeffs_lfe+max_sfb_lfeslots populated, emitting a leadingmono_data(b_lfe = 1)element (Table 21 +sf_info_lfe()Table 35) between the I-frame config block andcompanding_control(5)exactly where the decoder'sparse_7x_audio_data_outer(b_has_lfe = true)readsif (b_has_lfe) mono_data(1);. The on-wire body structure matches the existing round-118 7.1 ACPL_1 path — decoder resolvesSevenXCodecMode::AspxAcpl1withb_has_lfe = true, bothacpl_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. Total tests 766 (was 760). Real β extraction for the ACPL_2 / ACPL_3 paths and the round-128 ALPHA-writer negative-alpha_qdesync fix remain deferred. Round 144 closes the ACPL_2 5.0 half of the deferred list with the 5_X SIMPLE/ASPX_ACPL_2 encoder with real per-parameter-band α + β extraction per §4.2.6.6 Table 25 rowcase ASPX_ACPL_2:+ §5.7.7.5 Pseudocode 116 + §5.7.7.6.1 Pseudocode 117. NewAc4ImsEncoder::encode_frame_pcm_5_0_acpl2_real_alpha_beta(and the_with_max_sfbform) accepts a 5-channel[L, R, C, Ls, Rs]input and produces a 5_X ASPX_ACPL_2 frame whose two trailingacpl_data_1ch()elements carry real per-band α + β indices extracted from the (L, Ls) and (R, Rs) MDCT energy ratios via the round-128 / 132 shared analytic primitives. The on-wire body layout matches the round-100build_5_x_acpl2_body_from_pcm_spectraschedule (no joint-MDCT residual layer — ACPL_2 reconstructs the surround from L/R + the twoacpl_data_1ch()parameter sets at decode time); the Ls/Rs spectra are consumed only by the α + β extractors and are not transmitted.acpl_config_1ch(FULL)carries noqmf_band→start_band = 0so every parameter band participates in the α + β coding (in contrast to the ACPL_1 PARTIAL mode whoseacpl_qmf_bandmasks the low bands). Total tests 773 (was 766). Real β extraction for the ACPL_3 paths and the round-128 ALPHA-writer negative-alpha_qdesync fix (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) remain deferred. Round 174 closes the desync fix at the codebook contract level: ALPHA / BETA3 F0cb_offcorrected per §A.3 Tables A.34 / A.35 / A.46 / A.47. Pre-fixcb_off = 0conflicted with the §5.7.7.7 Pseudocode 121 differential-decoder / [acpl_synth::dequantize_alpha_index] signed-lane contract; the ALPHA F0 codebooks (17-entry Coarse / 33-entry Fine) are symmetric around the centre with a 1-bit peak at thealpha_q = 0lane, socb_off = N/2(8 Coarse / 16 Fine) is the right offset to read back signedalpha_q ∈ [-N/2, +N/2]directly fromdecode_delta. The fix lands in both [acpl::get_acpl_hcb] (decoder side) and the encoder's local [encoder_acpl3::acpl_hcb_arrays] mirror, plus the symmetric BETA3 F0 codebooks (cb_off = 4Coarse /8Fine — the §5.7.7.7dequantize_beta3multiplies the signed lane bybeta3_delta(qm)directly so they share ALPHA's signed convention). BETA F0 stays atcb_off = 0(unsigned magnitude — Table 204 / 206 stores positive entries only anddequantize_beta_indextakesunsigned_absthen re-applies the sign carried in by the differential accumulator). Three new unit tests ([alpha_f0_signed_lanes_round_trip_fine_and_coarse], [beta3_f0_signed_lanes_round_trip_fine_and_coarse], [alpha_f0_zero_alpha_picks_one_bit_peak]) sweep every signed lane through encode →decode_deltaand confirm the writer now picks the 1-bit symmetric peak foralpha_q = 0(down from 10 / 12 bits pre-fix). The round-128 family (encode_5_0_acpl1_real_alpha_emits_nonzero_alpha_when_surround_differs,..._symmetric_scaling_yields_matching_alpha) is re-shaped to assert on encoder byte-stream divergence rather than the decoder's recoveredalpha_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 (it manifests as a misalignment upstream ofparse_acpl_data_1ch, not in the ACPL F0 codeword itself). Total tests 776 (was 773). Round 181 closes the deferred "alpha_q desync" follow-up at two distinct spec-alignment layers. Layer 1: [acpl::parse_acpl_huff_data] now returns a spec-indexed length-num_param_bandsvector matching §5.7.7.7 Pseudocode 121'sacpl_<SET>[ps][i]shape (positions[0..start_band)are zero, F0 lands atvalues[start_band], DF deltas occupy[start_band+1..num_param_bands)); pre-r181's packed(num_bands - start_band)-length layout silently shifted the DIFF_FREQ accumulation for the PARTIALacpl_config_1chpath. Layer 2: [encoder_acpl3::write_aspx_data_2ch_minimal] now keys the SIGNAL ec_data band count offcfg.signals_freq_res()per §4.3.10.4.9 (Table 124 NOTE 3) — when the encoder doesn't emit an in-bandaspx_freq_resbit, the parser's high-res fallback selectsnum_sbg_sig_highresand the writer must match (pre-r181 it hard-codednum_sbg_sig_lowres, causing a 20-vs-10 SIGNAL desync that buried every subsequentacpl_data_1ch()α/β codeword in trailing zero-padding). End-to-end 5.0 ASPX_ACPL_2 asymmetric L/Ls input now recovers a non-zero per-bandalpha_qrow onacpl_data_1ch_pair[0/1]through the full PCM → MDCT → encode → AC-4 walker →differential_decodechain. Total tests 780 (was 776). The ASPX_ACPL_1 path retains a separate joint-MDCT residual-layer alignment issue between [encoder_acpl3::write_acpl_1_residual_layer] and the decoder'sparse_aspx_acpl_1_2_inner_bodyresidual-pair walker — tracked as the remaining follow-up. Round 187 pins that follow-up with four end-to-end characterisation tests (silence / L-only / Ls-only / combined) that sweep [encoder_ims::Ac4ImsEncoder::encode_frame_pcm_5_0_acpl1_real_alpha_beta] and assert each pair slot's recoveredacpl_data_1ch_pair[0/1].framing.num_param_setsso the next round can iterate on the residual / α-β writers without regressing the aligned silence / L-only / Ls-only paths. The diagnostic narrative intests/round187_acpl1_residual_desync_characterization.rstriangulates the drift surface: writer→parser pairs for [encoder_acpl3::write_acpl_data_1ch_real_alpha_beta_bytes] ↔ [acpl::parse_acpl_data_1ch] are bit-exact in isolation (already pinned byround181_alpha_desync_fix::standalone_*), so the remaining drift sits upstream of pair0 — either inwrite_acpl_1_residual_layervs the inline residual walk insideparse_aspx_acpl_1_2_inner_body's ASPX_ACPL_1 branch, or inwrite_two_channel_datavsparse_two_channel_data— when L and Ls are simultaneously non-trivial. Total tests 784 (was 780). Round 190 closes the desync at the root cause: the two minimal A-SPX writers ([encoder_acpl3::write_aspx_data_2ch_minimal] and [encoder_acpl3::write_aspx_data_1ch_minimal]) emittedaspx_int_class = FIXFIXas the wrong prefix code:0b11(2 bits) instead of0b0(1 bit) per ETSI TS 103 190-1 Table 126. The decoder's [aspx::AspxIntClass::read] walks the prefix correctly (0→ FixFix,10→ FixVar,110/111→ VarFix / VarVar), so the writer's11start drove the parser into the VarFix branch withb_iframe = 1and Note-1 2-bit width (num_aspx_timeslots = 15 > 8):var_bord_left(2 b) +num_rel_left(2 b) +tsg_ptr(2 b) — parser consumed 9 bits in framing where the writer emitted only 3. The 6-bit drift was masked in the silence / L-only / Ls-only paths (α / β quantised to 0 ⇒ constant minimum-costacpl_data_1chbodies whosenum_param_sets_codbit positions sampled0on both sides), but with non-zero α / β the codewords shifted and the pair-1num_param_sets_codbit position landed on a1(the r187 pinned failure mode). Fix is one-line per writer: emitbw.write_bit(false)for the FIXFIX prefix. The r187 pinned-broken test (acpl1_combined_l_and_ls_pair1_currently_misaligns) is nowacpl1_full_round_trips_with_aligned_pair_lengthsand assertspair1.num_param_sets = 1. Total tests 784 (unchanged from r187 — r190 fixed the third pin in place rather than adding new ones). Round 193 lifts the round-95 5_X ASPX_ACPL_3 encoder's β1 / β2 parameter sets out of the zero-delta scaffold: a new [encoder_acpl3::extract_beta_q_per_band_carrier_energy] extracts per-parameter-band β_q from a single carrier's MDCT energy distribution (β proportional to√E[x²]keeps the wet/dry balance bounded), and a sibling [encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_beta] drops it into the existingacpl_data_2ch()body in place of the two zero-deltaacpl_huff_data()codewords (α1 / α2 / β3 / γ1..γ6 stay at the round-95 minimum-bit-cost defaults). Caller-facing [encoder_ims::Ac4ImsEncoder::encode_frame_pcm_5_0_acpl3_real_beta] /encode_frame_pcm_5_1_acpl3_real_betawrap the new builder with the same channel-mode forcing and TOC framing as their round-95 counterparts. With α1 = α2 = 0 and β3 = 0 the §5.7.7.6.2 Pseudocode 119ACplModule2for the first parameter set reduces toz0 = 0.5·(x0·g1 + x1·g2 + y0·β1),z1 = 0.5·(x0·g1 + x1·g2 − y0·β1)(and analogously(z2, z3)with β2), so non-zero β1 / β2 injects the decorrelator output that gives the Ls / Rs outputs their decorrelated spaciousness. Seven integration tests intests/round193_5_x_acpl3_real_beta.rspin: round-trip to 5- / 6-channelAudioFramefor 5.0 / 5.1; silent input → all-zero β_q indices; tonal carrier + non-zerobeta_scale→ at least one non-zero β_q lane;beta_scale = 0.0is byte-for-byte identical to the round-95 scaffold (strict-superset invariant); silent inputs at anybeta_scaleare scaffold-identical; non-silent tonal inputs atbeta_scale > 0diverge from the scaffold (different β1 / β2 codeword bit-positions) while keeping the padded substream length identical. Total tests 791 (was 784). Round 196 layers real per-band α1 / α2 on top of r193: a new [encoder_acpl3::extract_alpha_q_per_band_carrier_correlation] drives α from the per-band L↔R normalised cross-correlationρ(L, R) = E[L·R] / √(E[L²]·E[R²])(α[pb] = α_scale · ρ), clamped to ALPHA_DQ ±2.0. The two ACplModule2 instances in ACPL_3 share the (L, R) carrier pair as their (x0, x1) input, so without a per-side surround reference α₁ and α₂ both receive the same correlation-policy output; the (L, Ls) ↔ (R, Rs) asymmetry stays carried by β1 ≠ β2 (from√E[L²]vs√E[R²]) and the two independent decorrelator outputs y0 / y1. A sibling [encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_alpha_beta] drops both the α and β extractors into theacpl_data_2ch()body; β3 / γ1..γ6 stay zero-delta. Caller-facing [encoder_ims::Ac4ImsEncoder::encode_frame_pcm_5_0_acpl3_real_alpha_beta] /encode_frame_pcm_5_1_acpl3_real_alpha_betawrap the new builder with the same channel-mode forcing and TOC framing. Mono-like (highly-correlated) bands push α toward +1, biasing more dry energy to the front pair; decorrelated bands stay near α = 0 and split the dry mix evenly. Four integration tests intests/round196_5_x_acpl3_real_alpha_beta.rspin: round-trip to a 5-channelAudioFrame; perfect L = R correlation quantises toα_q = +8(lane 24 = 1.0) at α_scale = 1; perfect anti-correlation L = -R quantises toα_q = −8;α_scale = β_scale = 0.0is byte-for-byte identical to the round-95 scaffold. Total tests 795 (was 791). Round 202 closes the ACPL_2 half of the deferred 7_X list with the 7.0 / 7.1 (3/4/0(.1)) SIMPLE/ASPX_ACPL_2 multichannel encoder with real per-parameter-band α + β extraction per ETSI TS 103 190-1 §4.2.6.14 Table 33 rowcase ASPX_ACPL_2:+ §5.7.7.5 Pseudocode 116 + §5.7.7.6.1 Pseudocode 117 — the 7_X (immersive) counterpart to the round-144 5.0 ACPL_2 real-α-β path and the real-α-β upgrade of the round-107 / 114 zero-delta 7_X ACPL_2 encoder. New [encoder_ims::Ac4ImsEncoder::encode_frame_pcm_7_0_acpl2_real_alpha_beta] and [encode_frame_pcm_7_1_acpl2_real_alpha_beta] (plus the_with_max_sfbforms) accept a 7- / 8-channel[L, R, C, Ls, Rs, Lb, Rb (, LFE)]input and produce a 7_X ASPX_ACPL_2 frame whose two trailingacpl_data_1ch()elements carry the analytic α + β indices extracted from the (L, Ls) and (R, Rs) MDCT pairs via the shared [encoder_acpl3::extract_alpha_q_per_band] / [extract_beta_q_per_band] primitives. New [encoder_acpl3::build_7_x_acpl2_body_from_pcm_spectra_real_alpha_beta] mirrors the round-107 zero-deltabuild_7_x_acpl2_body_from_pcm_spectrabody schedule (2-bit7_X_codec_mode = 3, optional LFEmono_data(b_lfe = 1), twotwo_channel_data()pairs, no joint-MDCT residual layer, trailing centremono_data(0),aspx_data_2ch + aspx_data_2ch + aspx_data_1chenvelope trailer) with the two trailingacpl_data_1ch_minimalwriters replaced bywrite_acpl_data_1ch_real_alpha_beta.acpl_config_1ch(FULL)carries noqmf_band→start_band = 0so every parameter band participates in α + β coding (in contrast to the ACPL_1 PARTIAL mode whoseacpl_qmf_bandmasks the low bands). D0 module models (L → Ls); D1 module models (R → Rs); the Ls / Rs spectra feed the α + β extractors only and are not emitted on the ACPL_2 wire. The back pair Lb / Rb is accepted for layout completeness but not carried by the ASPX_ACPL_2 body (the decoder's 7_X ACPL_2 dispatch populates slots 0..4 + the LFE slot 7 — slots 5/6 stay silent), matching the round-107 documented Table 202 channel mapping plus the round-80 LFE PCM render at decode time. Decoder round-trip: 7.0 ACPL_2 → 7-channel S16 interleaved PCM (1920 samples × 7 ch × 2 bytes); 7.1 ACPL_2 → 8-channel S16 with LFE IMDCT'd into slot 7. Ten integration tests intests/round202_7_x_acpl2_real_alpha_beta.rspin: 7.0 / 7.1 round-trip to 7- / 8-channelAudioFrame; decoder resolvesSevenXCodecMode::AspxAcpl2with bothacpl_data_1ch_pair[0/1]populated; loud-surround vs silence-surround inputs produce materially different bytes (the round-107 / 114 zero-delta scaffold would emit identical α / β codewords regardless of surround input); silence input round-trips with β_q = 0 in every band; encoder is bit-deterministic for matched inputs and fresh state; direct body-builder probe diverges from the round-107 zero-delta scaffold byte stream when the caller's Ls/Rs spectra are non-trivial. Total tests 805 (was 795). Round 208 lands the 5_X SIMPLE/ASPX_ACPL_3 encoder's real per-band γ5 / γ6 extraction — closing the centre- channel reconstruction half of the long-standing "γ stays at the zero-delta scaffold" deferral. In §5.7.7.6.2 Pseudocode 118 step 7 the centre outputz4is built by the thirdACplModule2invocation with(a = 1, b = 0, y = 0):z4 = 0.5 · (γ5·x0in + γ6·x1in). Step 11 scalesz4 *= √2before QMF synthesis; step 1 rescales the carriersx0in = (1 + √2)·L,x1in = (1 + √2)·R. The centre reconstruction (β3 = 0, ducker = 1) is thereforeC ≈ K · (γ5·L + γ6·R)withK = √2·(1+√2)/2 = 1+√(1/2). New [encoder_acpl3::extract_gamma_5_6_q_per_band_centre_least_squares] solves the 2×2 normal equations per parameter band ([<L,L>, <L,R>; <L,R>, <R,R>]·[γ5; γ6] = [<L,C>/K; <R,C>/K]) for the (γ5, γ6) pair that minimises the MDCT-bin-wise residualΣ (C/K − γ5·L − γ6·R)². Bands with a degenerate Gram matrix (no L or R energy, or perfectly collinear L = ±R within numerical tolerance) keep γ5 = γ6 = 0. The quantiser uses the Table-208 lineargamma_q = round(γ / gamma_delta)mapping with the symmetric±cb_offclamp (cb_off = 20Fine /10Coarse, table magnitude bound ±2.0). γ1..γ4 + β3 stay at the round-95 scaffold — those parameter sets drive the (L, R, Ls, Rs) sub-pipeline plus the ACplModule3 cross-residual, and neither has a per-side surround reference at encode time for the 5.0 / 5.1 PCM input layouts. New [encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_alpha_beta_gamma] is a drop-in replacement for the round-196 real-α-β builder with additionalcoeffs_c: Option<&[f32]>+gamma_scale: f32parameters. New [encoder_ims::Ac4ImsEncoder::encode_frame_pcm_5_0_acpl3_real_alpha_beta_gamma]_5_1_high-level entry points accept[L, R, C](5.0) or[L, R, C, LFE](5.1) PCM. Eight integration tests intests/round208_5_x_acpl3_real_gamma.rspin: 5.0 round-trip to a 5-channelAudioFrame; 5.1 round-trip to a 6-channelAudioFrame; silent-centre input produces γ5_q = γ6_q = 0 in every band;C = (L + R) / 2produces non-zero γ_q in ≥1 tonally-active band (verifies the least-squares extractor selects non-trivial γ); loud-centre vs silent-centre inputs produce materially different bytes (the round-196 path would emit identical γ codewords regardless of centre input);α/β/γ_scale = 0.0matches the round-95 scaffold byte-for-byte;γ_scale = 0.0reproduces the round-196 real-α-β bytes exactly; encoder is bit-deterministic for matched inputs and fresh state. Total tests 813 (was 805). Real γ1..γ4 extraction (the (L,R,Ls,Rs) sub-pipeline mix parameters) requires per-side surround references which the 5.0 / 5.1 PCM input layout does not carry — these stay at the round-95 zero-delta scaffold pending a 5.1+Ls+Rs PCM input layout. Real β extraction for the 7_X ACPL_3 paths, real ASPX envelope coding, real Table-181 SAP-derived residual content (for the ACPL_1 paths), and back-pair Lb/Rb carriage remain deferred. Round 215 closes the round-208 γ1..γ4 deferral by adding the 5_X SIMPLE/ASPX_ACPL_3 encoder's real per-band γ1 / γ2 / γ3 / γ4 extraction — the (L, Ls) and (R, Rs) output-pair gammas — driven by a 5-channel[L, R, C, Ls, Rs](5.0) or 6-channel[L, R, C, Ls, Rs, LFE](5.1) PCM input layout. In §5.7.7.6.2 Pseudocode 118 step 5 the (L, Ls) pair is built by the firstACplModule2invocation with(a = α₁, b = β₁, y = y₀):z0 = 0.5·(1+α₁)·(γ₁·x0in + γ₂·x1in) + 0.5·y₀·β₁andz1 = 0.5·(1−α₁)·(γ₁·x0in + γ₂·x1in) − 0.5·y₀·β₁, with step 11 scalingLs = √2·z1. Forming(L + Ls/√2)cancels they₀·β₁decorrelator contribution exactly, leavingL + Ls/√2 = (γ₁·x0in + γ₂·x1in) = (1+√2)·(γ₁·L + γ₂·R)via the step-1 carrier rescalingx0in / x1in = (1+√2)·L / R— independent of α₁ and β₁. By symmetry with step 6, the same fit shape gives(γ₃, γ₄)from(R + Rs/√2)/(1+√2). New [encoder_acpl3::extract_gamma_1_2_q_per_band_surround_least_squares] and [extract_gamma_3_4_q_per_band_surround_least_squares] solve the 2×2 normal equations[<L,L> <L,R>; <L,R> <R,R>]·[γ; γ'] = [<L,T>; <R,T>]per parameter band (the same shape as the round-208 γ5 / γ6 centre fit, just with a different per-band target). Bands with a degenerate Gram matrix keepγ = γ' = 0. The quantiser reuses the Table-208 lineargamma_q = round(γ / gamma_delta)mapping with the symmetric±cb_offclamp. New [encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_alpha_beta_full_gamma] is a drop-in replacement for the round-208 real-α-β-γ5-γ6 builder with additionalcoeffs_ls: Option<&[f32]>+coeffs_rs: Option<&[f32]>parameters and awrite_acpl_data_2ch_real_alpha_beta_full_gammahelper that emits all six γ entropy layers (γ1..γ6) as REAL codewords (β3 stays zero-delta). New [encoder_ims::Ac4ImsEncoder::encode_frame_pcm_5_0_acpl3_real_alpha_beta_full_gamma] and_5_1_high-level entry points accept[L, R, C, Ls, Rs](5.0) or[L, R, C, Ls, Rs, LFE](5.1) PCM. Nine integration tests intests/round215_5_x_acpl3_real_full_gamma.rspin: 5.0 round-trip to a 5-channelAudioFrame; 5.1 round-trip to a 6-channelAudioFrame; silent Ls (Ls = 0) → γ2_q = 0 in every band when probed directly; silent Rs (Rs = 0) → γ3_q = 0 in every band;α/β/γ_scale = 0.0matches the round-95 zero-delta scaffold byte-for-byte;γ_scale = 0.0reproduces the round-196 real-α-β bytes exactly; loud-surround vs silent-surround inputs produce materially different bytes (the round-208 path would emit identical γ1..γ4 codewords regardless of surround input); deterministic for matched inputs and fresh state. Total tests 822 (was 813). β3 extraction (requires modelling the unobservable third decorrelator outputy₂), real ASPX envelope coding, real Table-181 SAP-derived residual content (for the ACPL_1 paths), and back-pair Lb/Rb carriage remain deferred. Round 219 begins closing the "real ASPX envelope coding" deferral by landing the encoder's value-emitting ASPX-Huffman primitives — the six [encoder_acpl3::write_aspx_sig_f0_value] /write_aspx_sig_df_value/write_aspx_sig_dt_value/write_aspx_noise_f0_value/write_aspx_noise_df_value/write_aspx_noise_dt_valuehelpers — that the existing scaffold'spick_min_len_cw/pick_zero_delta_cwwriters can be swapped for in a follow-up round. Each takes an integer indexv(F0) or signeddelta_q(DF / DT) and writes the matching(cw, len)from the codebook selected by(quant_mode, stereo_mode)for SIGNAL paths orstereo_modealone for NOISE paths; values outside[0, codebook_length)(F0) or[-cb_off, +cb_off](DF / DT) clamp to the codebook's extreme entries rather than panicking, matching the decoder's parser semantics. The four SIGNAL codebooks (LEVEL_15/BALANCE_15/LEVEL_30/BALANCE_30) and the two NOISE codebooks (LEVEL/BALANCE) are all dispatched via a newaspx_sig_hcb_arrays()/aspx_noise_hcb_arrays()(LEN, CW, cb_off)triple lookup, mirroring the existingacpl_hcb_arrays()shape. Twelve integration tests intests/round219_aspx_envelope_value_writers.rspin: SIGNAL F0 / DF / DT round-trip againstparse_aspx_huff_data()for every(quant_mode, stereo_mode)× representative-value combination; NOISE F0 / DF / DT round-trip across both stereo modes; out-of-range F0 and DF values clamp to the codebook edge; repeated writes are byte-deterministic; and a full F0 + DF envelope round-trips through the higher-levelparse_aspx_ec_data()entry point withnum_sbg = 2andfreq_res = highres. Total tests 834 (was 822). The existing minimum-bit-costwrite_aspx_sig_f0/write_aspx_sig_df_zero/write_aspx_noise_f0/write_aspx_noise_df_zerowriters stay in place; nowrite_aspx_data_*_minimal()call site is touched. A subsequent round will route the new helpers through awrite_aspx_data_2ch_real_envelope()builder that consumes per-(sbg, atsg)envelope quant indices computed from the input MDCT spectra (inverting Pseudocode 82'sscf = n_subbands · 2^(qscf/a)form). Round 226 lands that builder pair. Two new public emitters in [encoder_acpl3] —write_aspx_data_2ch_real_envelope()(Table 52) andwrite_aspx_data_1ch_real_envelope()(Table 51) — accept a per-channel [encoder_acpl3::AspxRealEnvelopeChannel] payload (sig: &[i32]+noise: &[i32]) carrying caller-supplied F0 + signed DF quant indices and route them through the round-219 value-emitting helperswrite_aspx_sig_f0_value/write_aspx_sig_df_value/write_aspx_noise_f0_value/write_aspx_noise_df_value. The framing skeleton mirrors the existing_minimalwriters — FIXFIX prefix0,tmp_num_env = 0(→num_env = 1),aspx_balance = 1for the 2ch variant (shared channel-0 framing), SIGNAL + NOISE delta-direction bits = FREQ,aspx_hfgen_iwc_2ch/_1chtrailer all zeros — and the SIGNAL band count keys offcfg.signals_freq_res()(low-res when the in-bandaspx_freq_res = 0bit is emitted, otherwise the parser's high-res fallback). Per-channel stereo-mode follows Table 52: ch0 = LEVEL, ch1 = BALANCE; the 1ch path uses LEVEL throughout. Caller slices shorter than the derived SBG count zero-pad the trailing envelope positions; F0 values outside[0, codebook_length)clamp to the codebook edge; DF values outside[-cb_off, +cb_off]saturate to the symmetric edge — matching the round-219 helper semantics and the decoder'sdecode_delta()clamp surface. Eight integration tests intests/round226_aspx_real_envelope_writers.rspin: a 2ch deterministic F0 + DF envelope round-trips throughparse_aspx_ec_datato recover the caller's input per-channel; a 1ch envelope round-trips through the same path with LEVEL-only stereo_mode; short input slices zero-pad in place; the 2ch and 1ch writers are byte-deterministic across repeated invocations; all- zero inputs decode to all-zero envelopes; different per-channel inputs produce different bytes; out-of-range DF saturates at the codebook's+cb_offedge (Fine/Level DF cb_off = 70). Total tests 842 (was 834). The minimum-bit-costwrite_aspx_data_*_minimal()family stays in place; existing call sites are untouched. A follow-up round can chain the new builders with a per-(sbg, env) envelope-extractor that quantises the input MDCT spectra into the F0 + DF indices the new emitters accept (the inverse of Pseudocode 82'sscf = n_subbands · 2^(qscf/a)reconstruction). Round 234 closes that follow-up by adding the encoder-side ASPX envelope extractor — the inverse of ETSI TS 103 190-1 §5.7.6.3.4 Pseudocodes 80 / 81 (FREQ-direction DPCM accumulator) plus §5.7.6.3.5 Pseudocodes 82 (scf = n_subbands · 2^(qscf/a)) and 83 (scf_noise = 2^(6 − qscf_noise)). Five new public primitives in [encoder_acpl3] — [encoder_acpl3::quantize_sig_scf] (Fine⇒a = 2,Coarse⇒a = 1,num_qmf_subbands = 64), [encoder_acpl3::quantize_noise_scf], [encoder_acpl3::freq_dpcm_encode_qscf] (returns[F0, DF₁, …]withF0 = qscf[0]andDF[sbg ≥ 1] = qscf[sbg] − qscf[sbg − 1]), [encoder_acpl3::extract_aspx_sig_envelope_indices] and [encoder_acpl3::extract_aspx_noise_envelope_indices] — together compose the per-channel chainscf[] → qscf[] → [F0, DF₁, …]whose output is exactly theVec<i32>slice the round-226 builder pair accepts onAspxRealEnvelopeChannel::{sig, noise}. A new public type [encoder_acpl3::AspxEnvelopeScfChannel] ({ sig: &[f32], noise: &[f32] }) plus [encoder_acpl3::build_aspx_real_envelope_channel] runs both extractors and returns owned(Vec<i32>, Vec<i32>)for direct wiring. Non-positivescfclamps on the encoder side so the spec'sscf[0] = scf[1]carry-through path (Pseudocode 82 line) and callers passing 0 for silent bands stay well-defined; the round-219 writers further saturate any quant index outside[0, codebook_length)(F0) or[-cb_off, +cb_off](DF) at the codebook edge. The round-trip property is: feeding callerscfslices through the extractor, then the round-226 builder, then re-parsing the body throughparse_aspx_ec_dataplus the decoder'sdelta_decode_sig/delta_decode_noiseplusdequantize_sig_scf/dequantize_noise_scf, recovers the inputscfvectors within the per-band rounding ofround(a · log2(scf / 64))/round(6 − log2(scf)). Fourteen integration tests intests/round234_aspx_envelope_extractor.rspin: forward-inverse identity at integer-quant grid points for both Fine and Coarse signal step sizes; forward-inverse identity for Pseudocode 83 on the noise side; non-positivescfclamps to a finite quant index; FREQ-DPCM encoder produces[5, 2, −4, −4, 1]forqscf = [5, 7, 3, −1, 0]with the decoder's accumulator recovering the input exactly; empty / single-band inputs pass through; end-to-end accumulator + Pseudocode-82 / 83 round-trip from callerscfthrough extractor through Pseudocode-{82, 83};build_aspx_real_envelope_channelmatches direct calls entry-for-entry; full encoder→decoder loop wiringbuild_aspx_real_envelope_channelintowrite_aspx_data_2ch_real_enveloperecovers the per-channel SIGNAL / NOISEscfvectors through the decoder's full pipeline; determinism across repeated invocations; different inputs produce materially different DPCM payloads; empty per-channel slices return empty vectors. Total tests 856 (was 842). The encoder now has the completescf[] → on-wire byteschain for real ASPX envelope coding; remaining envelope-coding work is the energy estimator that turns input MDCT spectra into the per-sbgscfvectors the extractor consumes (the inverse of Pseudocodes 90 + 91), plus driving the new extractor + builder pair from the existing high-level encode entry points. Round 240 closes the first half of that follow-up by adding the encoder-side HF QMF energy aggregator — the dual of ETSI TS 103 190-1 §5.7.6.4.2.1 Pseudocodes 90 + 91 that the decoder uses on the inverse path. The aggregator [encoder_acpl3::aggregate_qmf_to_sbg_atsg] takes an HF QMF matrixq_highshaped[absolute_sb][ts], the SIGNAL or NOISE subband-group borders (sbg_sig/sbg_noiseper Pseudocode 91), the ATS-envelope borders (atsg_sig/atsg_noiseper Pseudocode 90), thenum_ts_in_atsATS span and the A-SPX start subbandsbx, and returns the per-[sbg][atsg]matrix of average squared magnitudes — i.e. the SBG-aggregated counterpart of the decoder's per-QMF-subbandest_sig_sb. Two thin per-side helpers [encoder_acpl3::extract_aspx_sig_envelope_scf_from_qmf] and [encoder_acpl3::extract_aspx_noise_envelope_scf_from_qmf] pick the leading envelope (atsg = 0) column for single-envelope frames, producing a per-sbgVec<f32>ready to feed the round-234 [encoder_acpl3::extract_aspx_sig_envelope_indices] / [encoder_acpl3::extract_aspx_noise_envelope_indices] extractors. A new public type [encoder_acpl3::AspxQmfEnvelopeChannel] ({ q_high: &[Vec<(f32, f32)>], sbg_sig_borders: &[u32], sbg_noise_borders: &[u32] }) plus [encoder_acpl3::build_aspx_real_envelope_channel_from_qmf] runs the aggregator + extractor pair end-to-end and returns owned(sig, noise) Vec<i32>ready to drop into the round-226AspxRealEnvelopeChannel { sig: &[i32], noise: &[i32] }slot. Edge handling tracks the decoder's bounds-checked Pseudocode 90: entries past the QMF matrix bounds contribute zero energy, empty / single-entry borders return empty vectors, zero-span ATS or band groups return0.0for the affected cell, andsbg_borders[i] < sbxclamps upward tosbxso callers can pass spec-shaped absolute borders verbatim. Fourteen integration tests intests/round240_aspx_qmf_energy_aggregator.rspin: constant-energy aggregation matches the per-cell mean; per-ATSG partitioning recovers a [1.0, 9.0] split across two envelopes; per-SBG partitioning recovers a [1.0, 16.0] split across two bands; sub-sbxborders clamp upward; empty SBG / ATSG borders return empty matrices; zero-span ATSG cells return 0.0; the SIGNAL + NOISE per-side helpers emit per-sbgvectors mirroring the aggregator; the QMF-driven convenience builder matches the manual aggregator + extractor + builder chain entry-for-entry; an integer- quant-grid input (scf = 64and128for Fine signal) hits the expected[F0 = 0, DF₁ = 2]DPCM payload; QMF rows shorter thantszcontribute partial energy without panicking; the QMF-driven builder is deterministic across repeated invocations; different QMF inputs produce different DPCM payloads. Total tests 870 (was 856). The encoder now has the completeq_high → scf → qscf → DPCM → on-wire byteschain for real ASPX envelope coding; remaining envelope-coding work is driving the new aggregator + extractor + builder chain from the existing high-level encode entry points (currently scaffolds emit minimum-cost zero-delta envelopes). Round 243 lands the encoder-sidechparam_info()/sap_data()builders — dual ofparse_chparam_info/parse_sap_dataper ETSI TS 103 190-1 §4.2.10.1 Table 47 + §4.2.10.2 Table 48. Before this round the encoder open-codedbw.write_u32(0, 2)at six sites inencoder_asf.rsfor identity-SAP (sap_mode = 0). [encoder_asf::write_chparam_info] now covers every legalsap_mode in {0, 1, 2, 3}:0is the existing identity emission,1walks per-(group, sfb)ms_used[g ][sfb]bits in group-major order,2(reserved) emits the 2-bit header on its own, mirroring the parser's accept-and-skip behaviour, and3dispatches into [encoder_asf::write_sap_data] which emits thesap_coeff_allbit, the per-pair flag array whensap_coeff_all = 0, the conditionaldelta_code_timebit whennum_window_groups != 1, and the per-pair HCB_SCALEFAC-codeddpcm_alpha_qdeltas (samedelta + 60 → HCB_SCALEFAC indexmap the round-49write_scalefac_datauses, with the same[0, 120]clamp policy). Half-builtChparamInfoinputs (rows shorter thanmax_sfb_per_group) zero-fill the missing entries so the writer stays total; asap_mode = 3input withsap_data = Noneemits aSapData::default()body that the parser walks as asap_coeff_all = 0all-false row. Thirteen integration tests intests/round243_chparam_info_writer.rspin everysap_modecode: header-only emissions (sap_mode in {0, 2}produce exactly 2 bits); single- and multi-groupms_usedpayloads recover entry-for-entry; missingms_usedrows zero-fill on the wire;sap_coeff_all = 1single-group bodies recover the DPCM deltas at even-sfb pair starts;sap_coeff_all = 0partial-pair bodies recover both the per-pair flag array and the selectively- emitted DPCM entries; multi-group bodies withdelta_code_time = 1recover across two groups; thesap_data = Nonedegenerate input emits a default body the parser walks; out-of-range DPCM deltas clamp to ±60; a full sweep of every legal delta in[-60, +60]round-trips exactly;sap_mode = 0drops a populated payload on emission; and in-memorysap_modevalues with high bits set are masked to the on-wire 2-bit field. Total tests 883 (was 870). The encoder now has a single reusable chparam-emission helper for all foursap_modecodes, ready for §4.2.10 SAP-mode decisioning (M/S vs. independent vs. joint-MDCT) to feed real per-bandms_used[]/ per-pair DPCM arrays into the existing 5_X / 7_X channel-element walkers in place of today's hard-coded identity-SAP literals. Round 246 lands the encoder-side Table-181 SAP residual extractor — a closed-form 2x2-per-sfb inverse of the §5.3.4.3.2 / Table 181 first-stage SAP matrix recovering joint-MDCT preliminary spectra(sSMP_A, sSMP_B, sSMP_3, sSMP_4)from a target preliminary set(L, R, Ls, Rs)plus achparam_info()pair. The forward path implemented in round 41 ([asf::apply_sap_table_181]) mixes the front-pair carriers(sSMP_A, sSMP_B)with the joint-MDCT residual(sSMP_3, sSMP_4)per Table 181's two independent 2x2 sub-systems; the new inverse ([asf::invert_sap_table_181]) reverses each sub-system usingdet = a*d - b*cand the closed-form[[d, -b], [-c, a]] / det. The three SAP coefficient families produced by [asf::extract_sap_abcd] all admit non-singular determinants — identity(1, 0, 0, 1)givesdet = 1, M/S(1, 1, 1, -1)givesdet = -2, and the SAP-coded(1 + g, 1, 1 - g, -1)withg = alpha_q * 0.1also givesdet = -2. A future spec extension with a singular row emits silence for that band instead of panicking (matching the forward path's graceful-degradation convention). Outside the SAP-coded extent the forward pass leaves(L, R) = (A, B)and zeros the surround pair; the inverse mirrors withA = L, B = R, s3 = s4 = 0so the round-trip is symmetric at the band boundary. ReturnsNonewhentransform_lengthlacks an entry insfb_offset_48, same failure mode as the forward path. Five new unit tests insrc/asf.rspin: identity-row inverse recovers(A = L, B = R, s3 = Ls, s4 = Rs)inside the SAP extent with passthrough + zero-surround outside; M/S-row inverse recovers the classic sum-and-differenceA = (L + Ls)/2, s3 = (L - Ls)/2over the SAP extent; forward-then-inverse round-trip is bit-stable on the identity row and tight to1e-5on the M/S row at f32; the unsupported-tl path returnsNone. Total lib tests 662 (was 657); integration tests unchanged (8 round91 7.X tests + 4 round95 5_X ACPL_3 tests still green). The IMS encoder's residual-layer builder ([encoder_acpl3::write_acpl_1_residual_layer]) currently emits rawsSMP,3 = Ls,sSMP,4 = Rsmatching the identitysap_mode = 0it writes; the new inverse opens the door to non-identity SAP modes producing the correct residual spectra for real psychoacoustic-driven joint-stereo decisions in subsequent rounds. Round 257 wires that door open: a new SAP-aware residual-layer writer ([encoder_acpl3::write_acpl_1_residual_layer_sap]) pairs the round-246 inverse with the round-243 [encoder_asf::write_chparam_info] emitter so the IMS encoder's §4.2.6.6 Table-25case ASPX_ACPL_1:residual layer can now express any of the three SAP coefficient families produced by [asf::extract_sap_abcd] — identity, M/S and SAP-codedalpha_q— driven by a caller-suppliedchparam_info()pair. The new path takes(coeffs_l, coeffs_r, coeffs_ls, coeffs_rs)preliminary spectra and anOption<&[ChparamInfo; 2]>: it emits the chparam pair viawrite_chparam_infowithmax_sfb_per_group = [max_sfb_master], recovers the residual(sSMP,3, sSMP,4)viainvert_sap_table_181, and writes the twosf_data(ASF)bodies. Whenchparam_pair = None(or both rows carrysap_mode = 0) the body is bit-equivalent to the legacy round-103 [encoder_acpl3::write_acpl_1_residual_layer] — the identity-row inverse reduces tos3 = ls, s4 = rs. A new public body builder ([encoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra_sap]) wraps the SAP-aware path with the same shape as the legacy [encoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra] plus the extrachparam_pairslot between the surround spectra and the ASPX config. Five new tests insrc/encoder_acpl3.rspin: bit-equivalence of the SAP-aware writer withchparam_pair = Noneagainst the legacy emitter; explicit-identity-rows == defaultNone; M/S-row body round-trips throughparse_chparam_infowith the expected per-bandms_usedrecovered; body-builder bit-equivalence withchparam_pair = None; full body fed throughparse_5x_audio_data_outerrecovers the chparam pair intotools.acpl_1_residual_chparam[0..1]withsap_mode = 1and the original per-band flags on both rows. Total lib tests 667 (was 662); integration suites unchanged. The decoder's round-30 pipeline already consumestools.acpl_1_residual_chparamthroughapply_sap_table_181to re-mix the surround spectra before IMDCT, so an encoder that drives the SAP-aware path now produces a stream that round-trips end-to-end through the existing decoder without further changes. Round 260 adds the encoder-sideChparamInfobuilders ([asf::build_chparam_info_ms_used] and [asf::build_chparam_info_sap_data_from_alpha_q]) — the two non-trivial-arm duals of [asf::extract_sap_abcd] (§5.3.4.3.2 / Pseudocode 59). The MsUsed builder wraps a per-(group, sfb)ms_usedflag matrix into aChparamInfowithsap_mode = 1; the SapData builder takes the desired per-(g, sfb)alpha_qindices in[-60, +60]plus per-pairsap_coeff_usedflags and computes the pair-major DPCMdpcm_alpha_q[g][sfb]deltas Pseudocode 59 accumulates back intoalpha_q— odd sfbs leave the dpcm slot at zero (decoder inherits from the pair-mate); even sfbs computecur - prevwith the samecode_deltapolicy as the decoder (code_delta == 1requiresg > 0,max_sfb_per_group[g] == max_sfb_per_group[g-1]and caller-supplieddelta_code_timeset, with referencealpha_q[g-1][sfb]; otherwisealpha_q[g][sfb-2]forsfb > 0and zero forsfb == 0). A fully-uniform "all set" matrix raisessap_coeff_allso the per-pair flag array elides;delta_code_timeis normalised tofalseon single-group payloads (Table 48 doesn't transmit the bit there). Five new unit tests insrc/asf.rspin:extract_sap_abcdreproduces the originalalpha_qrow on set bands and identity on cleared bands; the cross-groupdelta_code_timepath delivers the expecteddpcm_alpha_qdeltas; the single-groupdelta_code_time = trueinput is dropped tofalseon emit; andwrite_chparam_info→parse_chparam_inforecovers the same SAP body which extracts to the originalalpha_q. Total lib tests 674 (was 667); integration suites unchanged. Slots into the round-257 SAP-aware residual-layer writer — an IMS encoder that runs a psychoacoustic decision per parameter-band (M/S vs alpha-driven SAP joint stereo) can now materialise theChparamInfopair from its decision matrix instead of hand-crafting the innerSapDatabody. Round 263 completes thebuild_chparam_info_*family with the trivial third arm ([asf::build_chparam_info_none] — header-onlySapMode::None;extract_sap_abcdreproduces identity per-sfb across any per-group bound) and adds a per-(group, sfb) M/S-vs-L/R decision driver ([asf::select_ms_used_for_pair]) that picksms_used[g][sfb]per band using the standard joint-stereo concentration criterion: pick M/S whenmin(E_M', E_S') < min(E_L, E_R)over the per-band MDCT bins, withM' = (L + R) / 2, S' = (L - R) / 2(matching the per-sfb(1, 1, 1, -1)matrix the decoder'sSapMode::MsUsedarm applies). For a correlated pair M' carries the signal and S' vanishes (min_ms = 0 < min_lr); for an uncorrelated or anti-correlated pair both sit near(E_L + E_R) / 4. Ties (zero-energy bands, no concentration benefit) resolve tofalseso the encoder doesn't spend ams_usedbit when joint coding offers no concentration. The returnedVec<Vec<bool>>plugs directly intobuild_chparam_info_ms_usedand the result round-trips throughextract_sap_abcdto the per-sfb(1, 1, 1, -1)matrix on picked bands and identity on the rest. Five new unit tests insrc/asf.rscoverSapMode::Nonebuilder extract + bit-stream round-trip; per-band correlated / anti-correlated / one-sided / zero-energy decision discrimination; round-trip throughbuild_chparam_info_ms_used+extract_sap_abcd; respect of the per-groupmax_sfbbound; multi-group independence. Total lib tests 679 (was 674); integration suites unchanged. Together with round 260 this closes the encoder path for theSapMode::NoneandSapMode::MsUsedarms — an IMS encoder can now go directly from per-group L/R MDCT spectra to a fully-populatedChparamInfowithout hand-crafting the SAP matrix. Round 271 closes the last of the three non-reserved arms with the SAP-codedalpha_qdecision driver ([asf::select_alpha_q_for_pair]) — theSapMode::SapDataanalogue of round-263'sselect_ms_used_for_pair. Given target stereo MDCT spectra(L, R)it picks per-(group, sfb)alpha_q[g][sfb]indicessap_coeff_used[g][sfb]flags per §5.3.2 Pseudocode 59 + §5.3.3.2. The decoder reconstructs the output pair from the transmitted tracks via the SAP matrix(a, b, c, d) = (1 + g, 1, 1 - g, -1)withg = alpha_q · 0.1; inverting (det = -2) shows the encoder must transmitI_0 = M = (L + R) / 2andI_1 = S − g·MwithS = (L − R) / 2, so SAP coding is a one-tap prediction of the side track from the mid. Thegminimising the transmitted residual energyΣ (S[k] − g·M[k])²per parameter band is the least-squares projectiong* = ⟨S, M⟩ / ⟨M, M⟩, quantised byalpha_q = round(10·g*)and clamped to the HCB_SCALEFAC range[-60, +60].sap_coeff_usedis raised only when the quantised index is non-zero (pure-mid⟨S, M⟩ == 0and zero-mid-energy bands clear the flag — no SAP bit where prediction offers nothing). The even (pair-leading) sfb drives each(sfb, sfb+1)pair and the odd partner inherits, matching Pseudocode 59's pair-major copy. New public type alias [asf::SapAlphaDecision] for the(alpha_q, sap_coeff_used)pair. The returned matrices plug directly into the round-260 [asf::build_chparam_info_sap_data_from_alpha_q] builder, closing the encoder path from per-group L/R MDCT spectra to a fully-populatedSapMode::SapDataChparamInfo. Five new unit tests insrc/asf.rspin the least-squares projection (S = M → +10,S = -M → -10, odd-partner inheritance), flag-clearing on pure-mid / zero-energy bands, round-trip through the SapData builder +extract_sap_abcdto the(2, 1, 0, -1)matrix on picked bands,alpha_q = 60saturation, and multi-group independence. Total lib tests 684 (was 679); integration suites unchanged. Round 279 wires the decision drivers into the encoder proper: the decision-driven SAP-coded ASPX_ACPL_1 residual layer (§5.3.4.3.2 Table 181 + §5.3.2 Pseudocode 59). New [encoder_acpl3::select_acpl1_residual_chparam_pair] runs the round-271select_alpha_q_for_pairleast-squares decision per target(L, Ls)/(R, Rs)pair over the residual layer's single-group[max_sfb_master]layout (clampingalpha_qto ±30 so pair-major DPCM deltas stay HCB_SCALEFAC-codable) and materialises thechparam_info()rows via the round-260SapDatabuilder — falling back to the header-onlySapMode::Nonerow when no band benefits. New [encoder_acpl3::build_5_x_acpl1_body_from_pcm_spectra_sap_auto]- caller-facing [
encoder_ims::Ac4ImsEncoder::encode_frame_pcm_5_0_acpl1_sap] additionally fix the round-257 deferred carrier side: thetwo_channel_data()payload now carries the Table-181 matrix-input carriers(sSMP_A, sSMP_B) = (M, ·)recovered viainvert_sap_table_181(not the raw L/R preliminaries), so the decoder'sapply_sap_table_181forward mix reproduces the requested(L, R, Ls, Rs)exactly (up to sf_data quantisation). ForLs = κ·Lthe optimal projectiong* = (1−κ)/(1+κ)collapses the transmitted residualS − g·Mto near-silence — measured end-to-end: SAP residual energy < 5 % (unit) / < 10 % (full PCM→decoder integration) of the identity path's raw-Lsresidual on correlated tone fixtures, while a no-benefit input (Ls = L⇒g* = 0) encodes bit-for-bit identical to the round-103 identity path (strict-superset invariant). Five new unit tests + 4 integration tests (tests/round279_5_x_acpl1_sap_auto.rs) pin the selector's per-band(1.7, 1, 0.3, −1)extraction, the ±30 clamp, the identity fallback byte-equality, the bit-stream round trip (decoder recoverssap_mode = 3rows + forward mix matches all four preliminaries within 20 % relative L2), and the residual collapse. Total lib tests 689 (was 684); integration suites +4. Round 285 closes the round-215 "β₃ stays at the round-95 zero-delta scaffold" deferral with real per-parameter-band β₃ extraction for the 5_X SIMPLE/ASPX_ACPL_3 encoder — the last of the elevenacpl_data_2ch()parameter layers (α₁ α₂ β₁ β₂ β₃ γ₁..γ₆) to go real. Per §5.7.7.6.2 Pseudocode 118, β₃ gains the third decorrelator outputy₂into all three output pairs (steps 8-10); on the centre channel step 10 + step 11 give the wet contributionC_wet = −√2·0.5·β₃·y₂with energy0.5·β₃²·E[y₂²].y₂itself is decoder-side decorrelator state — unobservable at encode time — but its energy is observable: the decorrelator + ducker chain is energy-preserving in steady state soE[y₂²] ≈ E[v₃²], and the step-2 third-Transform drivev₃ = (γ₁+γ₃+γ₅)·x0in + (γ₂+γ₄+γ₆)·x1inis fully determined by the carrier spectra and the quantised γ matrix the encoder is already emitting. New [encoder_acpl3::extract_beta3_q_per_band_centre_residual] energy-matches the wet centre contribution against the per-band least-squares remainder of the round-208 dry fitE_res = Σ (C − K·(γ₅·L + γ₆·R))²(with the quantised γ₅ / γ₆ the decoder will apply), givingβ₃ = √(2·E_res / E[v₃²])— quantised per Table 207 (beta3_q = round(β₃ / beta3_delta), delta 0.125 Fine / 0.25 Coarse, symmetric clamp ±8 / ±4 per the staged ETSI table file's BETA3 F0 codebooks). New BETA3 value writers (write_acpl_beta3_f0_value/_df_value), a fullacpl_data_2ch()emitter with the β₃ layer live (write_acpl_data_2ch_real_alpha_beta_full_gamma_beta3), the drop-in builder [encoder_acpl3::build_5_x_acpl3_body_from_pcm_spectra_real_alpha_beta_full_gamma_beta3] (extrabeta3_scaleknob) and caller-facing [encoder_ims::Ac4ImsEncoder::encode_frame_pcm_5_0_acpl3_real_alpha_beta_full_gamma_beta3] /_5_1_entry points.beta3_scale = 0.0reproduces the round-215 full-γ bytes exactly. Four unit + six integration tests (tests/round285_5_x_acpl3_real_beta3.rs) pin the Table-207 quant grid + clamp, BETA3 F0/DF round-trip throughparse_acpl_huff_data- Pseudocode-121 accumulation, zero-residual ⇒ β₃ = 0 vs uncaptured-centre ⇒ β₃ > 0 decisioning, 5.0 → 5-ch and 5.1 → 6-ch decoder round-trips, decode-side recovery of the exact per-band
beta3_qrow throughparse_5x_audio_data_outer+differential_decode, byte-equality atbeta3_scale = 0, wire-liveness, and determinism. Total tests 929 (was 919). Remaining ACPL deferral: the 7_X back-pair Lb/Rb — §5.7.7.6.3 Table 202 maps the 7_X ASPX_ACPL_{1,2} A-CPL pair onto (Ls → Lb) / (Rs → Rb) with L / R / C as passthrough (z6 = x6,z7 = x7,z4 = x2in Pseudocode 120), whereas the current decoder render + encoder builders reuse the 5_X (L → Ls) / (R → Rs) mapping and leave the back-pair slots silent; lifting both sides onto the Table-202 mapping is the next 7_X round.Round 292 adds the encoder-side TIME-direction ASPX envelope DPCM — the dual of the
direction_time == truebranch of the decoder'sdelta_decode_sig/delta_decode_noise(§5.7.6.3.4 Pseudocode 80 / 81). Until now the round-219/226/234/240 envelope-coding chain only emitted the FREQ direction (freq_dpcm_encode_qscf: first-difference ofqscfacross subband groups, one envelope at a time). The decoder, though, carries a per-envelope direction flag and reconstructs the TIME branch asqscf[sbg][atsg] = prev[sbg] + delta·values[sbg], whereprevis the previous envelope's row (or the carried-overqscf_prev_lastfor the first envelope of the frame). New [encoder_acpl3::time_dpcm_encode_qscf] inverts that exactly —values[sbg] = (qscf[sbg] − prev[sbg]) / delta— with the same zero-extend-short-prevand±1step semantics; adelta = 0is treated as1so the helper stays total. New [encoder_acpl3::dpcm_encode_qscf_envelopes] packs a fullqscf[sbg][atsg]matrix into per-envelope [encoder_acpl3::AspxEncodedEnvelope]{ values, direction_time }rows, picking the cheaper transmission direction per envelope by minimisingΣ|values[sbg]|(the*_DF/*_DTcodebooks both peak at the zero-delta lane, so the smaller-magnitude row is the cheaper one); FREQ wins ties (no cross-envelope state needed), andforce_freqreproduces the legacy single-direction scaffold. Twelve integration tests (tests/round292_aspx_time_direction_dpcm.rs) pin the bit-exact round-trip through bothdelta_decode_siganddelta_decode_noise, the±1step anddelta = 0totality, short-prevzero-extension, the min-L1 direction policy (a temporally stable envelope codes TIME; a tie codes FREQ),force_freqmatchingfreq_dpcm_encode_qscfcolumn-for-column, and the empty-input edges. Total tests 941 (was 929). This closes the FREQ-only gap in the envelope-coding chain; driving the new multi-envelope packer from the high-level encode entry points (which still emit minimum-cost zero-delta single-envelope scaffolds) remains the open envelope follow-up.Round 299 lands the multi-envelope (
num_env > 1) ASPX body writers that consume the round-292 packer output — the missing link between [encoder_acpl3::dpcm_encode_qscf_envelopes] and the on-wireaspx_data_1ch()/aspx_data_2ch()bodies per ETSI TS 103 190-1 §4.2.12.3 Table 51 / §4.2.12.4 Table 52 + §4.2.12.5 Table 54 + §4.2.12.8 Table 57. The round-226 real-envelope writers (write_aspx_data_{1,2}ch_real_envelope) only ever emitted a single FIXFIX envelope (num_env == 1, FREQ direction); the new [encoder_acpl3::write_aspx_data_2ch_multi_envelope] and [encoder_acpl3::write_aspx_data_1ch_multi_envelope] emit FIXFIX bodies withtmp_num_envset so the decoder derivesnum_env = 1 << tmp_num_env(Table 123 / Table 126), per-envelopeaspx_delta_dirbits taken from each [encoder_acpl3::AspxEncodedEnvelope::direction_time] flag (num_envSIGNAL +num_noiseNOISE bits per channel), and per-envelope SIGNAL / NOISE ec_data honouring each envelope's chosen FREQ / TIME direction via two new direction-aware envelope writers (write_aspx_sig_envelope_directional/write_aspx_noise_envelope_directional, routing through the round-219 F0/DF/DT value helpers). A new public payload type [encoder_acpl3::AspxMultiEnvelopeChannel] ({ sig: &[AspxEncodedEnvelope], noise: &[AspxEncodedEnvelope] }) carries the per-envelope rows; short slices zero-pad missing envelopes (all-zero FREQ rows). Per Table 52 the SIGNAL quant step iscfg.quant_mode_envfornum_env > 1(the FIXFIX +num_env == 1→ Fine clamp does not apply — matching the decoder'sparse_aspx_data_2ch_bodyqmode resolution); NOISE is always Fine, andnum_noise = 2whennum_env > 1. The writers validate their preconditions (!signals_freq_res()— FIXFIX carries only one freq_res entry while SIGNAL ec_data walksnum_envenvelopes, so the high-res fallback must apply uniformly;num_enva power of two withinfixfix_tmp_num_env_bits()capacity) and return an error otherwise. Six integration tests intests/round299_aspx_multi_envelope_writers.rspin: 2ch and 1chnum_env = 2round-trip throughparse_aspx_framing+parse_aspx_delta_dir+parse_aspx_ec_data+delta_decode_sig/delta_decode_noiserecovering the caller's per-[sbg][atsg]qscfmatrices exactly; a temporally-stable second envelope packs as TIME and itsaspx_sig_delta_dir[1]bit reads backtrueoff the wire; short caller slices zero-pad; invalid configs are rejected; the writer is byte-deterministic. Total tests 947 (was 941). Driving the new multi-envelope body writers from the high-level encode entry points (which still emit single-envelope scaffolds) plus the QMF-energy → multi-envelopeqscfaggregation that selects the per-frame envelope count remain the open envelope follow-ups.Round 306 lands the encoder-side
aspx_hfgen_iwc_1ch()/aspx_hfgen_iwc_2ch()writers (ETSI TS 103 190-1 §4.2.12.6 / §4.2.12.7, Tables 55 / 56) — the exact duals of the decoder's [aspx::parse_aspx_hfgen_iwc_1ch] /parse_aspx_hfgen_iwc_2ch. Until now every encoder body writer emitted this HF-generation / interleaved-waveform-coding element as the all-zero compact form (aspx_tna_mode[*] = 0,aspx_ah_present = aspx_fic_present = aspx_tic_present = 0), even though the decoder fully parses real inverse-filtering modes, additive harmonics (add_harmonic), frequency-interleaved coding (fic_used_in_sfb) and time-interleaved coding (tic_used_in_slot). The new [encoder_acpl3::write_aspx_hfgen_iwc_1ch] / [encoder_acpl3::write_aspx_hfgen_iwc_2ch] take real per-SBGtna_mode(2 b, masked to0..=3) plus per-SBG / per-timeslot flag vectors via the public [encoder_acpl3::AspxHfgenIwc1ChPayload] / [encoder_acpl3::AspxHfgenIwc2ChPayload] payloads, and auto-derive every gate from the payload: each*_present/*_left/*_rightbit is1iff its slice has an active flag in range, and the 2ch TIC path emits the compactaspx_tic_copy = 1form when both channels carry the identical active pattern. Underaspx_balance = 1only channel-0tna_modeis written (the decoder mirrors it). Short caller slices zero-pad. The existingwrite_aspx_data_1ch_minimalHFGEN block is refactored to route through the new 1ch writer with a default (empty) payload — output stays byte-identical, confirmed by the unchanged minimal-writer tests. Eight integration tests intests/round306_aspx_hfgen_iwc_writers.rspin the bit-exact round-trip throughparse_aspx_hfgen_iwc_{1,2}ch: all-zero compact form, real flags, padding + masking, balance-mirror, distinct-tna, TIC-copy, TIC-right-only, and a full multi-field stress. Total tests 955 (was 947). Driving these real HFGEN/IWC decisions from the high-level encode entry points (which still emit the all-zero form) remains the open follow-up.
Specs
- ETSI TS 103 190-1 — Channel-based coding + bitstream syntax.
- ETSI TS 103 190-2 — Multi-stream / Immersive / Object-based (IFM).
Installation
[]
= "0.1"
= "0.1"
= "0.0"
What's parsed (TS 103 190-1 clause 4)
- Sync frame (
ac4_syncframe(), Annex G) —0xAC40plain or0xAC41CRC-protected, plus the two-tierframe_size()(16-bit,0xFFFFescape to 24-bit). - Raw frame (
raw_ac4_frame()). - Table of contents (
ac4_toc()): bitstream_version (withvariable_bits(2)escape for version == 3), sequence_counter, wait_frames,fs_index-> 44.1 / 48 kHz,frame_rate_index-> 24…120 fps + 23.44 (Table 83 / 84),b_iframe_global, payload_base. - Presentations: per-presentation
ac4_presentation_info()walking both thepresentation_v1(default) andpresentation_v0forms. Handlespresentation_config0..=5 (M+E+D, Main+DE, Main+Assoc, M+E+D+Assoc, Main+DE+Assoc, Main+HSF) plus thepresentation_config_ext_infoescape,b_hsf_ext,b_pre_virtualizedand additional EMDF substreams. - Substream info:
ac4_substream_info()channel mode (1/2/4/7-bit withvariable_bits(2)escape), sample-frequency multiplier, bitrate_indicator, content_type + language tag, per-frame-rate-factorb_iframeflags. - Substream index table: per-substream
substream_sizewith theb_more_bits/variable_bits(2)extension. - Bit-rate indicator / content classifier / frame_rate_factor /
sf_multiplier all surfaced on the parsed
Ac4FrameInfostruct.
What's not parsed yet
- ASF / ASF-A2 / A-SPX audio coefficient coding (the heart of the
codec). The A-SPX
aspx_config()header andcompanding_control()element are parsed (ETSI §4.2.11 / §4.2.12.1); the Huffman-coded envelope / noise payload (aspx_framing,aspx_ec_data, etc.) is not. - Metadata payloads inside substreams (DRC, dialog normalization,
downmix params) — the spec's
metadata()tree is skipped by size, not parsed. - TS 103 190-2 IFM (immersive / object) extensions.
- EMDF payload bodies — the outer
emdf_payloads_substream()walker (Table 18) andemdf_payload_config()(Table 79) are parsed but the per-payloademdf_payload_byte[]opaque sequence is captured as raw bytes; per-emdf_payload_idsemantic interpretation lives in the AC-4 EMDF datatype registry [i.14] and is out of scope for the present document.
Decode path
make_decoder builds an Ac4Decoder that:
- Scans the packet for a sync word.
- Parses the full TOC + presentation + substream descriptors, and
therefore knows the channel count, sample rate (44.1 / 48 kHz
scaled by
sf_multiplier), and frame length in samples. - Emits a silence
AudioFrame(S16 zeros) with the correctchannels,sample_rate,samplesandpts.
This is enough to keep a container/demuxer pipeline running against an AC-4 track without crashing, and to exercise the TOC parser against real fixtures.
Codec id
"ac4". Also registers the ISO BMFF fourcc ac-4 so MP4 tracks tagged
with the AC-4 sample entry resolve cleanly.
License
MIT — see LICENSE.